基于MVVM构建聊天App (二)登录UI实现

1、一个完整的开发流程

一般的,一个正常的流程包括:

  • 产品定需求,给出原型图
  • 团队确认需求
  • 由设计师开始设计图,同步开发做开发前的准备工作,如技术调查,前后端如何配合等
  • 设计师完成设计后,团队对比设计图和原型图再次确认需求
  • 开发团队开始开发工作
  • 开发完成后由开发者自测 一般我们在开发中编写的单元测试代码也属于自测
  • 开发者自测后由产品测试,事实上在开发过程中,产品也应该实时的关注开发
  • 交由专业测试人员做测试
  • 发布内测版本,做大规模测试
  • 提交App Store审核

2、storyboard的App启动过程

UIApplicationMain

一般的如果新建了一个Objective-C工程,默认先main函数,main函数内部会调用UIApplicationMain函数。

int main(int argc, char * argv[]) {
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

但在Swift项目中,并没有main.m文件也没有main函数。Swift工程是在AppDelegate中用一个@UIApplicationMain标签。那么UIApplicationMain做了哪些操作呢:

  • 创建UIApplication单例对象
  • 创建UIApplication的委托对象,即AppDelegate
  • 开启事件循环监听系统事件,当坚挺到对应的系统事件时会通知AppDelegate
  • 创建最底层的UIWindows对象,
  • 读取Info.plist中设置的默认启动storyboard文件名称
  • 加载设置的is Initial View Controllerstoryboard文件,同时创建对应的View Controller对象
  • 设置创建的View ControllerUIWindows的根视图rootViewController
  • 显示Windows上的试图

运行工程可以看到如下图: 最底层是一个UIWindwos

windows

3、没有storyboard的启动

一般的一个工程,默认从main.storyboard中的设置的Initial view controller启动的,那么我们该如何设置自己的初始启动View Controller呢?

在前面我们工程的结构:RPChat_iOS文件夹下是UI显示以及交互相关的代码,所以在RPChat_iOS中新建一个SignIn文件夹该目录下为登录相关的UI代码:

SignIn

新建登陆界面Controller命名为SignInViewController

AppDelegatedidFinishLaunchingWithOptions launchOptions回调方法中添加代码,设置SignInViewController为默认启动控制器:

if #available(iOS 13, *) {
    
} else {
     window = UIWindow.init()
     window?.frame = UIScreen.main.bounds
     window?.makeKeyAndVisible()
     let signInVC = SignInViewController()
     window?.rootViewController = signInVC
}

SceneDelegateoptions connectionOptions方法中添加代码:

guard let windowScene = (scene as? UIWindowScene) else { return }

window = UIWindow(frame: windowScene.coordinateSpace.bounds)
window?.windowScene = windowScene
window?.backgroundColor = .white
let tabBar = SignInViewController()
window?.rootViewController = tabBar
window?.makeKeyAndVisible()

4、登录UI的实现

先看一下这是最终效果图:

light Mode
  • 1、Auto Layout

至于UI,考虑到版本兼容和后期维护我采用了系统NSLayoutAnchor适配,

NSLayoutAnchor常用属性

  • leadingAnchor
  • trailingAnchor
  • leftAnchor
  • rightAnchor
  • topAnchor
  • bottomAnchor
  • widthAnchor
  • heightAnchor
  • centerXAnchor
  • centerYAnchor
  • firstBaselineAnchor
  • lastBaselineAnchor

关于Auto Layout其他更多使用细节请参考官方文档:

High Performance Auto Layout

Apple Develope NSLayoutConstraint

Apple Develope NSLayoutAnchor

WWDC 2018 What's New in Cocoa Touch

  • 2、UI实现

新建一个View命名SignInRootView作为登录界面的主View,采用懒加载的方式初始化视图

最顶部的Logo图片实现代码:

lazy var logoImg: UIImageView = {
        self.addSubview($0)
        $0.translatesAutoresizingMaskIntoConstraints = false
        let top = $0.topAnchor.constraint(equalTo: self.safeAreaLayoutGuide.topAnchor, constant: 44)
        let centerX = $0.centerXAnchor.constraint(equalTo: self.centerXAnchor, constant: 0)
        let width = $0.widthAnchor.constraint(equalToConstant: 120)
        let height = $0.heightAnchor.constraint(equalToConstant: 120)
        NSLayoutConstraint.activate([top, centerX, width, height])
        return $0
    }(UIImageView())

用户名输入框实现代码:

 lazy var accountNumberView: UIView = {
        self.addSubview($0)
        $0.translatesAutoresizingMaskIntoConstraints = false
        let top = $0.topAnchor.constraint(equalTo: logoImg.bottomAnchor, constant: 20)
        let left = $0.leftAnchor.constraint(equalTo: self.leftAnchor, constant: 40)
        let right = $0.rightAnchor.constraint(equalTo: self.rightAnchor, constant: -40)
        let height = $0.heightAnchor.constraint(equalToConstant: 50)
        NSLayoutConstraint.activate([top, left, right, height])
        $0.layer.cornerRadius = 25
        return $0
    }(UIView())
 
    
    lazy var accountNumberLab: UITextField = {
        accountNumberView.addSubview($0)
        $0.translatesAutoresizingMaskIntoConstraints = false
        $0.topAnchor.constraint(equalTo: accountNumberView.topAnchor, constant: 0).isActive = true
        $0.leftAnchor.constraint(equalTo: accountNumberView.leftAnchor, constant: 16).isActive = true
        $0.rightAnchor.constraint(equalTo: accountNumberView.rightAnchor, constant: -16).isActive = true
        $0.bottomAnchor.constraint(equalTo: accountNumberView.bottomAnchor, constant: 0).isActive = true
        $0.font = UIFont.init(name: "PingFangTC-Semibold", size: 19)
        return $0
    }(UITextField())

密码输入框实现代码:

lazy var inputPasswordView: UIView = {
           self.addSubview($0)
           $0.translatesAutoresizingMaskIntoConstraints = false
           let top = $0.topAnchor.constraint(equalTo: accountNumberView.bottomAnchor, constant: 20)
           let left = $0.leftAnchor.constraint(equalTo: self.leftAnchor, constant: 40)
           let right = $0.rightAnchor.constraint(equalTo: self.rightAnchor, constant: -40)
           let height = $0.heightAnchor.constraint(equalToConstant: 50)
           NSLayoutConstraint.activate([top, left, right, height])
           $0.layer.cornerRadius = 25
           return $0
       }(UIView())
    
       
    lazy var inputPasswordTxt: UITextField = {
           inputPasswordView.addSubview($0)
           $0.translatesAutoresizingMaskIntoConstraints = false
           $0.topAnchor.constraint(equalTo: inputPasswordView.topAnchor, constant: 0).isActive = true
           $0.leftAnchor.constraint(equalTo: inputPasswordView.leftAnchor, constant: 16).isActive = true
           $0.rightAnchor.constraint(equalTo: inputPasswordView.rightAnchor, constant: -16).isActive = true
           $0.bottomAnchor.constraint(equalTo: inputPasswordView.bottomAnchor, constant: 0).isActive = true
           $0.font = UIFont.init(name: "PingFangTC-Semibold", size: 19)
           return $0
       }(UITextField())

登录按钮实现代码:

lazy var signInBtn: UIButton = {
        self.addSubview($0)
        $0.translatesAutoresizingMaskIntoConstraints = false
        let top = $0.topAnchor.constraint(equalTo: inputPasswordView.bottomAnchor, constant: 20)
        let left = $0.leftAnchor.constraint(equalTo: self.leftAnchor, constant: 40)
        let right = $0.rightAnchor.constraint(equalTo: self.rightAnchor, constant: -40)
        let height = $0.heightAnchor.constraint(equalToConstant: 50)
        NSLayoutConstraint.activate([top, left, right, height])
        $0.layer.cornerRadius = 25
        $0.titleLabel?.font = UIFont.init(name: "PingFangTC-Semibold", size: 20)
        $0.setTitle(NSLocalizedString("Sign In", comment: ""), for: .normal)
        return $0
   }(UIButton())

此处代码较多,具体实现请看代码: gitub RPChat

  • 3、设置背景颜色

由于设计师给出的颜色一般为16进制,此处需要做一个转码处理:

public class func hexStringToColor(_ hexadecimal: String) -> UIColor {
        var cstr = hexadecimal.trimmingCharacters(in:  CharacterSet.whitespacesAndNewlines).uppercased() as NSString;
        if(cstr.length < 6){
            return UIColor.clear;
        }
        if(cstr.hasPrefix("0X")){
            cstr = cstr.substring(from: 2) as NSString
        }
        if(cstr.hasPrefix("#")){
            cstr = cstr.substring(from: 1) as NSString
        }
        if(cstr.length != 6){
            return UIColor.clear;
        }
        var range = NSRange.init()
        range.location = 0
        range.length = 2
        let rStr = cstr.substring(with: range);
        range.location = 2;
        let gStr = cstr.substring(with: range)
        range.location = 4;
        let bStr = cstr.substring(with: range)
        var r :UInt32 = 0x0;
        var g :UInt32 = 0x0;
        var b :UInt32 = 0x0;
        Scanner.init(string: rStr).scanHexInt32(&r);
        Scanner.init(string: gStr).scanHexInt32(&g);
        Scanner.init(string: bStr).scanHexInt32(&b);
        return UIColor.init(red: CGFloat(r)/255.0, green: CGFloat(g)/255.0, blue: CGFloat(b)/255.0, alpha: 1)
    }

然后就可以用设计师给的16进制设置背景颜色:

signInBtn.backgroundColor = UIColor.hexStringToColor("0xF5BE62")
  • 4、Drak Mode适配

(1)、文本和UIView颜色适配

由于iOS 13之后苹果处理Drak Mode,作为开发者也应该做相应的兼容处理。现在我在代码中并没有此时调整模拟器为暗模式,运行工程可以看到在暗模式下,用户名和密码输入框背景色不见了。此时就应该做暗模式的兼容处理。

drak mode

此处我的做法也很简单,对UIColorextension处理,然后再扩展方法中分别对Drak ModeLight Mode两种模式做对应的处理,代码如下:

extension UIColor {
   /// 当前是否是暗模式
    public class var drakMode: Bool {
        if #available(iOS 13.0, *) {
            let currentMode = UITraitCollection.current.userInterfaceStyle
            if currentMode == .dark {
                return true
            }
        }
        return false
    }
    public class func isDrakMode() -> Bool {
        if #available(iOS 13.0, *) {
            let currentMode = UITraitCollection.current.userInterfaceStyle
            if currentMode == .dark {
                return true
            }
        }
        return false
    }
    /// UIView背景颜色
    public class var darkModeViewColor: UIColor {
        if #available(iOS 13.0, *) {
            return .systemBackground
        } else {
            return .white
        }
    }
    public class func configDarkModeViewColor() -> UIColor {
        if #available(iOS 13.0, *) {
            return .systemBackground
        } else {
            return .white
        }
    }
    /// 文字颜色
    public class var darkModeTextColor: UIColor {
        if #available(iOS 13.0, *) {
            return UIColor{(trainCollection) -> UIColor in
                if trainCollection.userInterfaceStyle == .dark {
                    return .white
                } else {
                    return .black
                }
            }
        } else {
            return .black
        }
    }
    public class func configDarkModeTxtColor() -> UIColor {
        if #available(iOS 13.0, *) {
            return UIColor{(trainCollection) -> UIColor in
                if trainCollection.userInterfaceStyle == .dark {
                    return .white
                } else {
                    return .black
                }
            }
        } else {
            return .black
        }
    }
    /// 子UIView背景颜色
    public class var subDarkModeViewColor: UIColor {
        if #available(iOS 13.0, *) {
            return UIColor{(trainCollection) -> UIColor in
                if trainCollection.userInterfaceStyle == .dark {
                    return UIColor(red: 100/255, green: 100/255, blue: 100/255, alpha: 1)
                } else {
                    return .groupTableViewBackground
                }
            }
        } else {
            return .groupTableViewBackground
        }
    }
    public class func configSubDarkModeViewColor() -> UIColor {
        if #available(iOS 13.0, *) {
            return UIColor{(trainCollection) -> UIColor in
                if trainCollection.userInterfaceStyle == .dark {
                    return UIColor(red: 100/255, green: 100/255, blue: 100/255, alpha: 1)
                } else {
                    return .groupTableViewBackground
                }
            }
        } else {
            return .groupTableViewBackground
        }
    }
    /// 设置默认带颜色的view背景
    public class func configDarkModeViewWith(_ dfaultColor: UIColor) -> UIColor {
        if #available(iOS 13.0, *) {
            return UIColor{(trainCollection) -> UIColor in
                if trainCollection.userInterfaceStyle == .dark {
                    return UIColor(red: 100/255, green: 100/255, blue: 100/255, alpha: 1)
                } else {
                    return dfaultColor
                }
            }
        } else {
            return dfaultColor
        }
    }
    /// 设置带默认颜色的文字颜色
    public class func configDarkModeTxtColorWith(_ dfaultColor: UIColor) -> UIColor {
        if #available(iOS 13.0, *) {
            return UIColor{(trainCollection) -> UIColor in
                if trainCollection.userInterfaceStyle == .dark {
                    return .systemBackground
                } else {
                    return dfaultColor
                }
            }
        } else {
            return dfaultColor
        }
    }
    /// 设置Placeholder文字颜色
    public class var placeholderColor: UIColor {
        if #available(iOS 13.0, *) {
            return UIColor{(trainCollection) -> UIColor in
                if trainCollection.userInterfaceStyle == .dark {
                    return UIColor(red: 255, green: 255, blue: 255, alpha: 0.25)
                } else {
                    return UIColor(red: 0, green: 0, blue: 0, alpha: 0.25)
                }
            }
        } else {
            return UIColor(red: 0, green: 0, blue: 0, alpha: 0.25)
        }
    }
    public class func configPlaceholderColor() -> UIColor {
        if #available(iOS 13.0, *) {
            return UIColor{(trainCollection) -> UIColor in
                if trainCollection.userInterfaceStyle == .dark {
                    return UIColor(red: 255, green: 255, blue: 255, alpha: 0.25)
                } else {
                    return UIColor(red: 0, green: 0, blue: 0, alpha: 0.25)
                }
            }
        } else {
            return UIColor(red: 0, green: 0, blue: 0, alpha: 0.25)
        }
    }
  
}

只需要在设置颜色时通过方法或者属性设置即可:

accountNumberView.backgroundColor = .subDarkModeViewColor
inputPasswordView.backgroundColor = .subDarkModeViewColor
accountNumberView.backgroundColor = UIColor.configSubDarkModeViewColor()
inputPasswordView.backgroundColor = UIColor.configSubDarkModeViewColor()

从结果上来说,这两种方式没有任何区别。一般用属性的话在设置或计算时看起来更自然一些,例如我用 a = 0会比a = getData() 看起来更直观自然一些。
《Clearn Code》一书中强调,对方法或函数的命名尽量使用动词或动词短语,所以在使用一个方法时,通常的代码表示要做一些事情。此处我只是修改UIView的背景颜色,所以我个人觉得使用属性设置更直观一些。

(2)、图片DrakMode适配

在Xcode中修改图片为Any,Dark,Xcode会自动生成一组新的暗模式下的填充图片,把图片拖入即可,系统会自动根据当前的模式来设置图片。

DrakMode
drak Mode

(3)、监听traitCollection的变化

在一些特殊的地方需要根据当前系统是否处于暗模式分别作出处理,如一般情况下隐私协议用WKWebView加载,在正常情况下为白色背景黑色文字,在暗模式下为黑色背景白色文字,此时上面的代码已经不能满足我的需求了。此时就需要对当前系统是否是暗模式做监听处理。

我的做法是定一个DrakModeProtocol协议,在需要监听的Controller中,遵循这个协议即可。

/// 协议
public protocol DrakModeProtocol : NSObjectProtocol {
    @available(iOS 13.0, *)
    var traitCollection: UITraitCollection { get }

    
    @available(iOS 13.0, *)
    func traitCollectionDidChange(_ traitCollection: UITraitCollection?)
}
/// 使用方法
extension MineWKWebViewController: DrakModeProtocol {
    override func traitCollectionDidChange(_ traitCollection: UITraitCollection?) {
           if #available(iOS 13.0, *) {
               if UITraitCollection.current.userInterfaceStyle == .dark {
                   webView.evaluateJavaScript("document.body.style.backgroundColor=\"#000000\"") { (data, error) in
                       
                   }
                   webView.evaluateJavaScript("document.body.style.webkitTextFillColor=\"#FFFFFF\"") { (data, error) in
                       
                   }
               } else {
                   webView.evaluateJavaScript("document.body.style.backgroundColor=\"#FFFFFF\"") { (data, error) in
                       
                   }
                   webView.evaluateJavaScript("document.body.style.webkitTextFillColor=\"#000000\"") { (data, error) in
                       
                   }
               }
           }
       }
}

隐私协议
隐私协议

再次运行项目,可以看到界面已经兼容了暗模式:

drak Mode

本文主要写了:

  • 不通过storyboard启动App
  • 登录UI的实现
  • Drak Mode的适配

本文demo: Github RPChat

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,425评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,058评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,186评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,848评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,249评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,554评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,830评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,536评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,239评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,505评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,004评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,346评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,999评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,060评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,821评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,574评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,480评论 2 267

推荐阅读更多精彩内容