千亿级IM独立开发指南丨全球即时通讯全套代码4小时速成(二):UI设计与搭建

本文篇幅较长,预计阅读时长30-60min,欢迎收藏+点赞+关注。

这是《千亿级IM独立开发指南!全球即时通讯全套代码4小时速成》的第二篇:《UI设计与搭建》

系列文章可参考:

千亿级IM独立开发指南丨全球即时通讯全套代码4小时速成(一):功能设计与介绍

千亿级IM独立开发指南丨全球即时通讯全套代码4小时速成(三):App内部流程与逻辑(上)

千亿级IM独立开发指南丨全球即时通讯全套代码4小时速成(三):App内部流程与逻辑(下)

千亿级IM独立开发指南丨全球即时通讯全套代码4小时速成(四):服务端搭建与总结

二、UI 设计与搭建

基于第一篇的功能和模块规划,我们先来搭建UI,这样能尽快地看到软件最终的样子。
为了快速搭建和开发,本篇将以 iOS 为平台,选用 SwiftUI 和 Swift 进行开发。Android 平台上的 Kotlin 版本,请参见后续文章。

1. 创建iOS项目

万事开头难,我们先来创建一个基本的 SwiftUI 项目。
打开 XCode,出现XCode欢迎界面:

点击“Create a new Xcode project”,进入模板选择界面:

选择 “iOS”,“Application”部分选择“App”即可。
点击“Next”,进入项目选项页面:

“Interface”选择“SwiftUI”,“Language”选择“Swift”。
点击“Next”,进入项目存储选择页面:

选择合适的存储位置,点击“Create”,创建项目。进入开发界面:

界面中,XCode已经根据App模板,创建了“IMDemoApp”和“ContentView”两个页面。本篇及后续的讲解,将基于该项目,继续进行。

2. 登录页面

一般而言,打开一个App,用户最先看到的将是登录页面,所以我们将从登录页面,开始我们的讲解。
首先在左侧项目导航窗口中,右键点击“IMDemo”,弹出右键菜单,选择“New File...”:

打开文件模板选择窗口。在“User Interface”部分,选择“SwiftUI View”,点击“Next”:

在文件存储对话框中,将文件改名为“LoginView”,点击“Create”创建文件:

创建后的 LoginView 如图所示:

登陆界面一般会有五个核心元素:

  • 用户头像/默认头像
  • 用户账号输入控件
  • 用户登录密码输入控件
  • “登陆”按钮
  • 对于非注册用户,转到注册界面的“注册”按钮

于是我们修改 LoginView 的代码如下:

import SwiftUI

struct LoginView: View {
    @State private var username: String = ""
    @State private var password: String = ""
    
    @State private var userImage: String = ""
    
    func changeToRegisterView() {
        //-- TODO: change to register view
    }
    
    func userLogin(){
        //-- TODO: user login
    }
    
    var body: some View {
        VStack(alignment: .center){
            
            Image(self.userImage)
                .resizable()
                .aspectRatio(contentMode: .fit)
                .frame(width:UIScreen.main.bounds.width/2, height: UIScreen.main.bounds.width/2, alignment: .center)
                .padding()
            
            HStack(alignment: .center){
                Spacer()
                Text("用户名:")
                    .padding()
                TextField("用户名", text: $username)
                    .autocapitalization(.none)
                    .padding()
                Spacer()
            }
            HStack(alignment: .center){
                Spacer()
                Text("密 码:")
                    .padding()
                SecureField("登陆密码", text: $password)
                    .padding()
                Spacer()
            }
            HStack(alignment: .center){
                Button("登 陆"){
                    userLogin()
                }.frame(width: UIScreen.main.bounds.width/4,
                height: nil)
                .padding(10)
                .foregroundColor(.white)
                .background(.blue)
                .cornerRadius(10)
                
                Button("注册"){
                    changeToRegisterView()
                }
                .padding(10)
            }
        }
    }
}

struct LoginView_Previews: PreviewProvider {
    static var previews: some View {
        LoginView()
    }
}

我们用一个VStack对界面元素进行组织。从上到下依次是用户头像,用户账号输入部分,用户登录密码输入部分,以及“登陆”和“注册”按钮。

这个时候,我们突然发现一件事情,无论我们想要跳转到注册页面,还是想提取已注册的用户头像,都需要视图之外的模块进行辅助。于是,我们将先添加几个独立于页面视图的 Swift 代码,提供对相应功能的支持。

3. 独立于页面的支持代码

于是我们先添加两个额外的Swift文件,先添加相关的代码,以便为UI的搭建,提供对应的功能支持。
还是在XCode左侧项目导航窗口,右键点击“IMDemo”,弹出右键菜单,选择“New File...”,打开文件模板选择窗口。在“Source”部分,选择“Swift File”,添加swift代码文件:

两个文件分别保存为“Config.swift”和“IMCenter.swift”。如图所示:

编辑 Config.swift,修改内容如下:

import Foundation

class IMDemoUIConfig {
    static let defaultIcon = "livedatalogo"
    static let topNavigationHight = 50.0
    static let bottomNavigationHight = 50.0
    static let navigationIconEdgeLen = 44.0
    
    static let contactItemHight = 60.0
    static let contactItemImageEdgeLen = 50.0
}

defaultIcon 是当用户还未选择头像时的默认头像。于是,我们索性先把需要的图像资源添加到位。

注:相关的图像资源可以从本篇末的项目地址中获取。

因为我们一共有6个页面,所以我们在 IMDemoApp.swift 中简单添加一个枚举,便于后续页面切换。

enum IMViewType {
    case LoginView
    case RegisterView
    case SessionView
    case ContactView
    case ProfileView
    case DialogueView
}

因为存在在页面中共享的数据,为了避免后续功能增改时分散传递和修改的麻烦,我们在 IMDemoApp.swift 中添加一个新的类,打包处理一下:

class ViewSharedInfo: ObservableObject {
    @Published var currentPage: IMViewType = .LoginView
}

替换 IMDemoApp.swift 中 IMDemoApp 的 body 相关代码,以便进行视图切换。最终修改的IMDemoApp.swift代码如下:

import SwiftUI

enum IMViewType {
    case LoginView
    case RegisterView
    case SessionView
    case ContactView
    case ProfileView
    case DialogueView
}

class ViewSharedInfo: ObservableObject {
    @Published var currentPage: IMViewType = .LoginView
}

@main
struct IMDemoApp: App {
    
    @StateObject var sharedInfo: ViewSharedInfo = IMCenter.viewSharedInfo
    
    var body: some Scene {
        WindowGroup {
            switch sharedInfo.currentPage {
            case .LoginView:
                LoginView()
            case .RegisterView:
                RegisterView()
            case .SessionView:
                SessionView()
            case .ContactView:
                ContactView()
            case .ProfileView:
                ProfileView()
            case .DialogueView:
                DialogueView()
            }
        }
    }
}

然后修改 IMCenter.swift代码如下:

import Foundation
import UIKit
import SwiftUI

class IMCenter {
    static var viewSharedInfo: ViewSharedInfo = ViewSharedInfo()
    
    //-- 存储用户属性
    class func storeUserProfile(key: String, value: String) {
        UserDefaults.standard.set(value, forKey: key)
    }
    //-- 获取用户属性
    class func fetchUserProfile(key: String) -> String {
        let value = UserDefaults.standard.string(forKey: key)
        return ((value == nil) ? "" : value!)
    }
    
    class func loadUIIMage(path:String)-> UIImage {
                
        let fullPath = NSHomeDirectory() + "/Documents/" + path
        let fileUrl = URL(fileURLWithPath:fullPath)
        let data = try? Data(contentsOf: fileUrl)
        if data == nil {
            print("load image from disk failed. path: \(fileUrl)")
            return UIImage(named: IMDemoUIConfig.defaultIcon)!
        }
        return UIImage(data: data!)!
    }
    
}

除了 storeUserProfile() 和 fetchUserProfile() 是存取用户属性外,我们再增加一个函数 loadUIIMage(),用于将保存在设备上的图像,加载到内存。

最后,创建项目时根据模版创建的 ContentView 此时已经不在有用,可以直接删除。

4. 登录页面(继续)

有了前面的基础,我们继续编辑 LoginView。

首先,我们希望如果之前有用户登录过,那就先默认显示上次登录用户的头像,以及预填用户名。如果之前没有用户登录过,再显示默认头像。于是增加初始化函数:

    init() {
        let username = IMCenter.fetchUserProfile(key: "username")
        self.userImage = IMCenter.fetchUserProfile(key: "\(username)-image")
    }

其次,当用户完成输入的时候,键盘有可能会挡住“登陆”按钮,所以我们需要添加 hideKeyboard() 函数。
而类似的需求只要有输入便必定存在。为了便于后续页面的开发,我们将 hideKeyboard() 函数添加到 IMDemoApp.swfit 中:

#if canImport(UIKit)
extension View {
    func hideKeyboard() {
        UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil)
    }
}
#endif

再次修改 LoginView.swift:

struct LoginView: View {
    var body: some View {
        VStack(alignment: .center){
            
            ... ...
            
        }
        .onTapGesture {
            hideKeyboard()
        }
    }
}

然后,当用户登录的时候,用户账号/用户名和密码是不允许为空的。如果遇到类似的情况发生,我们需要给出提示:

@State private var alertTitle: String = ""
@State private var errorMessage: String = ""
@State private var showAlert = false

... ...

                Button("登 陆"){
                    hideKeyboard()
                    userLogin()
                }.frame(width: UIScreen.main.bounds.width/4,
                height: nil)
                .padding(10)
                .foregroundColor(.white)
                .background(.blue)
                .cornerRadius(10)
                .alert(alertTitle, isPresented: $showAlert) {
                    Button("确认") {
                        self.showAlert = false
                    }
                } message: {
                    Text(errorMessage)
                }

... ...

第四,登陆是一个网络过程,期间可能会有数秒的等待。于是我们需要添加一个提示页面:

@State private var showLoginingHint = false

... ...

                Button("登 陆"){
                    hideKeyboard()
                    userLogin()
                }
                ... ...
                .alert(alertTitle, isPresented: $showAlert) {
                    ... ...
                }
                .fullScreenCover(isPresented: $showLoginingHint, onDismiss: {
                    //-- Do nothing.
                }) {
                    VStack {
                        Text("登录中,请等待....")
                            .font(.title)
                    
                        ProgressView()
                    }
                }

... ...

如果登录失败了,也需要有类似的提示:

@State private var loginFailed = false

... ...

                Button("登 陆"){
                    hideKeyboard()
                    userLogin()
                }
                ... ...
                .alert(alertTitle, isPresented: $showAlert) {
                    ... ...
                }
                .fullScreenCover(isPresented: $showLoginingHint, onDismiss: {
                    //-- Do nothing.
                }) {
                    ... ...
                }
                .fullScreenCover(isPresented: $loginFailed, onDismiss: {
                    //-- Do nothing.
                }) {
                    VStack {
                        Text("登录失败")
                            .font(.title)
                        
                        Text(errorMessage).padding()
                        
                        Button("确定") {
                            self.loginFailed = false
                        }
                        .frame(width: UIScreen.main.bounds.width/4,
                        height: nil)
                        .padding(10)
                        .foregroundColor(.white)
                        .background(.blue)
                        .cornerRadius(10)
                    }
                }

... ...

当我们修改用户账号的时候,如果新的账号在本机上曾经登陆过,那我们希望显示该账号最后一次登陆时的头像:

    ... ...

    func usernameEditing(editing: Bool) {
        if editing == false {
            self.userImage = IMCenter.fetchUserProfile(key: "\(self.username)-image")
        }
    }

    ... ...

    var body: some View {
        
        ... ...
        
                    TextField("用户名", text: $username, onEditingChanged: usernameEditing)
                    .autocapitalization(.none)
                    .onAppear() {
                        self.username = IMCenter.fetchUserProfile(key: "username")
                    }
                    .padding()
        
        ... ...
    }

    ... ...

最终 LoginView.swift 代码如下:

import SwiftUI

struct LoginView: View {
    @State private var username: String = ""
    @State private var password: String = ""
    
    @State private var alertTitle: String = ""
    @State private var errorMessage: String = ""
    @State private var showAlert = false
    @State private var showLoginingHint = false
    @State private var loginFailed = false
    
    @State private var userImage: String
    
    init() {
        let username = IMCenter.fetchUserProfile(key: "username")
        self.userImage = IMCenter.fetchUserProfile(key: "\(username)-image")
    }
    
    func changeToRegisterView() {
        IMCenter.viewSharedInfo.currentPage = .RegisterView
    }
    
    func userLogin(){
        
        if username.isEmpty {
            
            self.alertTitle = "无效输入"
            self.errorMessage = "用户名不能为空!"
            self.showAlert = true
            
            return
        }
        
        if password.isEmpty {
            
            self.alertTitle = "无效输入"
            self.errorMessage = "用户密码不能为空!"
            self.showAlert = true
            
            return
        }
        
        self.showLoginingHint = true
        
        //-- TODO: user login
        IMCenter.viewSharedInfo.currentPage = .SessionView
    }
    func usernameEditing(editing: Bool) {
        if editing == false {
            self.userImage = IMCenter.fetchUserProfile(key: "\(self.username)-image")
        }
    }
    
    var body: some View {
        VStack(alignment: .center){
            
            if self.userImage.isEmpty {
                Image(IMDemoUIConfig.defaultIcon)
                    .resizable()
                    .aspectRatio(contentMode: .fit)
                    .frame(width:UIScreen.main.bounds.width/2, height: UIScreen.main.bounds.width/2, alignment: .center)
                    .padding()
            } else {
                Image(uiImage: IMCenter.loadUIIMage(path: self.userImage))
                    .resizable()
                    .aspectRatio(contentMode: .fit)
                    .frame(width:UIScreen.main.bounds.width/2, height: UIScreen.main.bounds.width/2, alignment: .center)
                    .padding()
            }
            
            HStack(alignment: .center){
                Spacer()
                Text("用户名:")
                    .padding()
                TextField("用户名", text: $username, onEditingChanged: usernameEditing)
                    .autocapitalization(.none)
                    .onAppear() {
                        self.username = IMCenter.fetchUserProfile(key: "username")
                    }
                    .padding()
                Spacer()
            }
            HStack(alignment: .center){
                Spacer()
                Text("密 码:")
                    .padding()
                SecureField("登陆密码", text: $password)
                    .padding()
                Spacer()
            }
            HStack(alignment: .center){
                Button("登 陆"){
                    hideKeyboard()
                    userLogin()
                }.frame(width: UIScreen.main.bounds.width/4,
                height: nil)
                .padding(10)
                .foregroundColor(.white)
                .background(.blue)
                .cornerRadius(10)
                .alert(alertTitle, isPresented: $showAlert) {
   
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值