Swift-MVVM 简单演练(三)

Swift-MVVM 简单演练(一)git

Swift-MVVM 简单演练(二)github

Swift-MVVM 简单演练(四)json

优化一些小细节

设置SVProgressHUD最小提示时间

在咱们用SVProgressHUD的时候,它默认的显示时长可能会不符合你的使用规则。咱们能够更改它显示的最小时间(setMinimumDismissTimeInterval)swift

像这种全局都能用到的东西,咱们最好是设置在一个方便管理的地方,这里以在AppDelegate中设置api

extension AppDelegate {

    fileprivate func setupAddtions() {

        // 设置`SVProgressHUD`最小解除时间
        SVProgressHUD.setMinimumDismissTimeInterval(1)
    }
}复制代码

设置AFN指示器

不少好的应用程序是很是人性化的,若是有网络请求的时候,会在状态栏的位置有一个Loading的很小的标志,这是苹果自带的标志,其实咱们应该把它在应该显示的时候显示出来的。幸运的是,咱们遇上了一个好的时代。AFN这个框架已经帮咱们实现了。网络

extension AppDelegate {

    fileprivate func setupAddtions() {

        // 设置网络加载指示器
        AFNetworkActivityIndicatorManager.shared().isEnabled = true
    }
}复制代码

这里须要强调一下,如今不管是移动网络仍是无线网络,网速愈来愈快了(咱们遇上了一个好的时代)。若是网速很快的时候,即便是设置了这个,通常也是看不到的。可是网速很差的时候,它就起做用了。app

将询问发送通知受权的代码也抽取出来

swiftextension是能够无限多个写的,咱们若是能将更多的零碎的方法抽取出来,放到extension中去。代码会清晰不少,也会方便管理不少。框架

extension AppDelegate {

    fileprivate func setupNotification() {

        if #available(iOS 10.0, *) {
            UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .badge, .sound, .carPlay]) { (sucess, error) in
// print("受权" + (sucess ? "成功" : "失败"))
            }
        } else {
            // Fallback on earlier versions
            let notificationSettings = UIUserNotificationSettings(types: [.alert, .badge, .sound], categories: nil)
            UIApplication.shared.registerUserNotificationSettings(notificationSettings)
        }
    }
}复制代码

值得注意的是,以前下面这段代码原本是这样的ssh

} else {
    // Fallback on earlier versions
    let notificationSettings = UIUserNotificationSettings(types: [.alert, .badge, .sound], categories: nil)
    application.registerUserNotificationSettings(notificationSettings)
}复制代码

若是放到extensionapplication是须要当作参数传递过去的,而咱们本着省事的原则,直接使用UIApplication.shared就能够了,UIApplication是单例,只要用的时候直接取出它就能够了。async


处理登陆相关通知

Tokennil时测试

全部的网络请求都是基于token的,若是没有token的话(虽然实际程序中几乎不可能出现token = nil的状况),咱们应该使程序在当token = nil而且用户又一次进行了网络请求的时候将提示用户,而且将登陆控制器展示出来。

HQNetWorkManager中,发送登陆通知

/// 带`token`的网络请求方法
func tokenRequest(method: HQHTTPMethod = .GET, URLString: String, parameters: [String: AnyObject]?, completion: @escaping (_ json: Any?, _ isSuccess: Bool)->()) {

    // 判断`token`是否为`nil`,为`nil`直接返回,程序执行过程当中,通常`token`不会为`nil`
    guard let token = userAccount.token else {

        // 发送通知,提示用户登陆
        print("没有 token 须要从新登陆")
        NotificationCenter.default.post(
            name: NSNotification.Name(rawValue: HQUserShouldLoginNotification),
            object: nil)
        completion(nil, false)
        return
    }复制代码

写的任何代码都要测试,随便找一个控制器的viewDidLoad方法里面。将token置为nil

class HQDViewController: HQBaseViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        HQNetWorkManager.shared.userAccount.token = nil
    }复制代码

接下来再回到首页,下拉刷新。因为又进行了网络请求,并且咱们判断了当tokennil时的判断,所以会发送一个登陆的通知。在HQMainViewController中,以前咱们添加了监听的方法

class HQMainViewController: UITabBarController {

    override func viewDidLoad() {
        super.viewDidLoad()

        NotificationCenter.default.addObserver(self, selector: #selector(login), name: NSNotification.Name(rawValue: HQUserShouldLoginNotification), object: nil)复制代码

所以,监听到通知,就会走login的方法,弹出登陆界面了。

// MARK: - Targrt Action
extension HQMainViewController {

    // MARK: - 登陆监听方法
    @objc fileprivate func login(n: Notification) {

        print("用户登陆通知 \(n)")

        SVProgressHUD.setDefaultMaskType(.clear)
        let nav = UINavigationController(rootViewController: HQLoginController())
        self.present(nav, animated: true, completion: nil)
    }复制代码

Token的过时处理

HQNetWorkManager内目前就两个方法,并且仍是有关联的,因此处理完第一个方法的时候,咱们理应看下第二个方法。若是token不为nil,咱们该在什么地方作何处理呢?

这里根据请求失败的返回码处理一下,当statusCode == 403时,咱们再次发送用户登陆的通知

/// 封装 AFN 的 GET/POST 请求
///
/// - Parameters:
/// - method: GET/POST
/// - URLString: URLString
/// - parameters: parameters
/// - completion: 完成回调(json, isSuccess)
func request(method: HQHTTPMethod = .GET, URLString: String, parameters: [String: AnyObject]?, completion: @escaping (_ json: Any?, _ isSuccess: Bool)->()) {

    let success = { (task: URLSessionDataTask, json: Any?)->() in
        completion(json, true)
    }

    let failure = { (task: URLSessionDataTask?, error: Error)->() in

        if (task?.response as? HTTPURLResponse)?.statusCode == 403 {
            print("token 过时了")

            // 发送通知,提示用户再次登陆
            NotificationCenter.default.post(
                name: NSNotification.Name(rawValue: HQUserShouldLoginNotification),
                object: "bad token")
        }

        print("网络请求错误 \(error)")
        completion(nil, false)
    }复制代码

任何状况都要进行测试,再次回到以前的测试控制器里面,给token赋值一个非空的值测试

class HQDViewController: HQBaseViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        HQNetWorkManager.shared.userAccount.token = "bad token"
    }复制代码

若是咱们再次回到首页控制器,进行网络请求,就会再次弹出登陆界面。

处理弹出登陆界面的一些UI细节

若是咱们不作一些提示,或者动画过分一下的话,直接就硬生生弹出登陆控制器,逻辑上没有问题,可是交互老是感受不那么好。所以咱们最好作一点小提示。

可是在哪里作提示比较好呢。建议仍是放在接收到登陆通知的监听方法里面处理比较好。

首先,咱们发送登陆通知的时候,附带一个自定义的object(这里是字符串"bad token")过去。

// 发送通知,提示用户再次登陆
NotificationCenter.default.post(
    name: NSNotification.Name(rawValue: HQUserShouldLoginNotification),
    object: "bad token")复制代码

而后在处理监听登陆通知的方法里处理交互显示的问题,仅仅是增长一点点提示的UI而已,有了下面的代码,交互就会感受好了不少了。这里主要学习的是若是忽然增长需求,咱们如何在合适的位置处理问题。

// MARK: - Targrt Action
extension HQMainViewController {

    // MARK: - 登陆监听方法
    @objc fileprivate func login(n: Notification) {

        print("用户登陆通知 \(n)")

        if n.object != nil {
            SVProgressHUD.setDefaultMaskType(.gradient)
            SVProgressHUD.showInfo(withStatus: "登陆超时,请从新登陆")
        }

        DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 2) {

            SVProgressHUD.setDefaultMaskType(.clear)
            let nav = UINavigationController(rootViewController: HQLoginController())
            self.present(nav, animated: true, completion: nil)
        }
    }复制代码

看看本身为了完成某一需求而改的代码,有没有影响到其它地方

时刻提醒本身,当咱们兴高采烈的为完成了某一处的改动而沾沾自喜的时候。要在对其它有可能会被影响的地方测试一下。否则,往后遗留的问题可能会让你百思不得其解。

这不就,咱们刚为了处理token过时而设置的延迟两秒钟再弹出登陆界面,果真就影响到了其它的登陆地方。

好比,一开始没有登陆的时候,运行程序,会出现登陆注册的按钮。当咱们点击登陆的按钮的时候,咱们指望马上弹出登陆控制器。

可是咱们刚才写的代码,真的有影响到这里了。点击登陆也是延迟2秒钟才弹出登陆界面,给人的感受老是怪怪的。

下面咱们想办法测试一下

将存储用户帐户相关的文件删除

而后运行程序,就直接到登陆界面,而后点击登陆按钮发现老是须要等待2秒钟,咱们找到以前延迟两秒钟的地方处理一下。

增长一个时间变量,若是token过时了,就将时间增减2秒,不然不增长。

// MARK: - Targrt Action
extension HQMainViewController {

    // MARK: - 登陆监听方法
    @objc fileprivate func login(n: Notification) {

        print("用户登陆通知 \(n)")

        var when = DispatchTime.now()

        if n.object != nil {
            SVProgressHUD.setDefaultMaskType(.gradient)
            SVProgressHUD.showInfo(withStatus: "登陆超时,请从新登陆")

            // 修改延迟时间
            when = DispatchTime.now() + 2
        }

        DispatchQueue.main.asyncAfter(deadline: when) {

            SVProgressHUD.setDefaultMaskType(.clear)
            let nav = UINavigationController(rootViewController: HQLoginController())
            self.present(nav, animated: true, completion: nil)
        }
    }复制代码

这样就能够解决普通登陆状态下的展示登陆界面的延迟问题了。


加载用户我的信息

获取用户我的信息数据

接口地址

/// 我的信息
let HQUserInfoUrlString = "https://api.weibo.com/2/users/show.json"复制代码

HQNetWorkManager+Extension中增长用户我的信息获取的网络请求方法

// MARK: - 用户信息
extension HQNetWorkManager {

    /// 加载用户信息
    func loadUserInfo(completion: @escaping (_ dict: [String: AnyObject]) -> ()) {

        guard let uid = userAccount.uid else {
            return
        }
        let params = ["uid": uid]

        tokenRequest(URLString: HQUserInfoUrlString, parameters: params as [String : AnyObject]) { (json, isSuccess) in

            // 完成回调
            completion(json as? [String : AnyObject] ?? [:])
        }
    }
}复制代码

那么问题来了,此方法在哪里调用比较合适呢?

由于,咱们须要拿到这个在首页就展现昵称或者头像。因此在登陆成功可是没有执行完成回调的时候去执行该方法获取用户我的信息是比较理想的位置。

下面我这里并无作网络请求交互获取token,只是模拟了一下而已。

// MARK: - 请求`Token`
extension HQNetWorkManager {

    /// 根据`账号`和`密码`获取`Token`
    ///
    /// - Parameters:
    /// - account: account
    /// - password: password
    /// - completion: 完成回调
    func loadAccessToken(account: String, password: String, completion: @escaping (_ isSuccess: Bool)->()) {

        // 从`bundle`加载`data`
        let path = Bundle.main.path(forResource: "userAccount.json", ofType: nil)
        let data = NSData(contentsOfFile: path!)

        // 从`Bundle`加载配置的`userAccount.json`
        guard let dict = try? JSONSerialization.jsonObject(with: data! as Data, options: []) as? [String: AnyObject]
            else {
                return
        }

        // 直接用字典设置`userAccount`的属性
        self.userAccount.yy_modelSet(with: dict ?? [:])

        self.userAccount.saveAccount()

        // 加载用户信息
        self.loadUserInfo { (dict) in
            print(dict)
            // 用户信息加载完成再执行,首页数据加载的完成回调
            completion(true)
        }

    }
}复制代码

保存所须要的我的信息(昵称、头像地址)

获取到我的信息以后,这种我的信息可能会在不少地方须要用到,咱们最好将其像保存token那样将其保存起来。

所以,扩展一下我的信息模型,增长两个属性

/// 用户昵称
var screen_name: String?
/// 用户头像地址(大图),180x180
var avatar_large: String?复制代码

HQNetWorkManager+Extension中的请求token的方法里保存,以前只是保存了tokenuidexpires_in(过时时间),如今须要将新获取到的screen_nameavatar_large(头像地址)也保存到此

func loadAccessToken(account: String, password: String, completion: @escaping (_ isSuccess: Bool)->()) {

    // 从`bundle`加载`data`
    let path = Bundle.main.path(forResource: "userAccount.json", ofType: nil)
    let data = NSData(contentsOfFile: path!)

    // 从`Bundle`加载配置的`userAccount.json`
    guard let dict = try? JSONSerialization.jsonObject(with: data! as Data, options: []) as? [String: AnyObject]
        else {
            return
    }

    // 直接用字典设置`userAccount`的属性
    self.userAccount.yy_modelSet(with: dict ?? [:])

    // 加载用户信息
    self.loadUserInfo { (dict) in

        self.userAccount.yy_modelSet(with: dict)
        self.userAccount.saveAccount()

        // 用户信息加载完成再执行,首页数据加载的完成回调
        completion(true)
    }复制代码

和以前的对比一下,应该会看的更清楚


更改导航栏标题显示样式

以前微博的版本和如今多少有点区别,在首页的导航栏的标题位置仅仅是显示本身的昵称,而且可下拉展开。这里不去作那么复杂,只是表达一下,更改导航栏标题显示样式和Button的文字图片左右对调,以前我也写过Objective-C的相关方法iOS-自定义 UIButton-文字在左、图片在右(一)iOS-自定义 UIButton-文字在左、图片在右(二)

将导航栏标题设置成自定义Button

这个没什么技术含量,直接上代码了。

/// 设置导航栏标题演示
    fileprivate func setupNavTitle() {

        let btn = UIButton(hq_title: "王红庆", fontSize: 17, normalColor: UIColor.darkGray, highlightedColor: UIColor.red)
        btn.setImage(UIImage(named: "nav_arrow_down"), for: .normal)
        btn.setImage(UIImage(named: "nav_arrow_up"), for: .selected)
        navItem.titleView = btn

        btn.addTarget(self, action: #selector(clickTitleButton), for: .touchUpInside)
    }

    @objc fileprivate func clickTitleButton(btn: UIButton) {

        btn.isSelected = !btn.isSelected
    }复制代码

抽取建立相似标题按钮的逻辑

相似这种需求可能一个项目中不止一个地方会用到,即使是目前就这一个地方会用到,咱们也应该尽可能将其抽取出来。由于要设置图像和文字,而且颠倒其位置的这些代码,应该封装起来的。只留给使用者(包括咱们本身)一个快速建立此按钮的方法就能够了。

我选择在ButtonExtension中搞定这个。

/// 文字在左、图片在右的 Button
class HQTitleButton: UIButton {

    /// 重载构造函数
    ///
    /// - Parameter title: title 若是是 nil,就显示首页
    /// - Parameter title: title 若是不是 nil,显示 title 和 箭头
    init(title: String?) {
        super.init(frame: CGRect())

        if title == nil {
            setTitle("首页", for: .normal)
        } else {
            setTitle(title!, for: .normal)
            setImage(UIImage(named: "nav_arrow_down"), for: .normal)
            setImage(UIImage(named: "nav_arrow_up"), for: .selected)
        }

        titleLabel?.font = UIFont.boldSystemFont(ofSize: 17)
        setTitleColor(UIColor.darkGray, for: .normal)

        // 设置大小
        sizeToFit()
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}复制代码

这样咱们设置的时候就能够简化不少,目前尚未实现将文字和图片颠倒

/// 设置导航栏标题演示
fileprivate func setupNavTitle() {

    let title = HQNetWorkManager.shared.userAccount.screen_name

    let btn = HQTitleButton(title: title)

    navItem.titleView = btn

    btn.addTarget(self, action: #selector(clickTitleButton), for: .touchUpInside)
}

@objc fileprivate func clickTitleButton(btn: UIButton) {

    btn.isSelected = !btn.isSelected
}复制代码

利用layoutSubViews方法从新调整按钮文字和图像的位置

在调用override func layoutSubviews()方法的时候,必定要调用super.layoutSubviews(),若是不调用,就会出现显示不出来的状况。

/// 文字在左、图片在右的 Button
class HQTitleButton: UIButton {

    /// 重载构造函数
    ///
    /// - Parameter title: title 若是是 nil,就显示首页
    /// - Parameter title: title 若是不是 nil,显示 title 和 箭头
    init(title: String?) {
        super.init(frame: CGRect())

        if title == nil {
            setTitle("首页", for: .normal)
        } else {
            setTitle(title! + " ", for: .normal)
            setImage(UIImage(named: "nav_arrow_down"), for: .normal)
            setImage(UIImage(named: "nav_arrow_up"), for: .selected)
        }

        titleLabel?.font = UIFont.boldSystemFont(ofSize: 17)
        setTitleColor(UIColor.darkGray, for: .normal)

        // 设置大小
        sizeToFit()
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    /// 从新布局子视图
    override func layoutSubviews() {
        super.layoutSubviews()

        // 判断`label`和`imageView`是否同时存在
        guard let titleLabel = titleLabel,
            let imageView = imageView
            else {
                return
        }

        // 将`titleLabel`的`x`向左移动`imageView`的`width`,值得注意的是,这里咱们须要将`width / 2` 
        titleEdgeInsets = UIEdgeInsetsMake(0, -imageView.bounds.width, 0, imageView.bounds.width)
        // 将`imageView`的`x`向右移动`titleLabel`的`width`,值得注意的是,这里咱们须要将`width / 2`
        imageEdgeInsets = UIEdgeInsetsMake(0, titleLabel.bounds.width, 0, -titleLabel.bounds.width)
        /********** 下面这种作法不推荐 **********/
        // 会有问题
// titleLabel.frame = titleLabel.frame.offsetBy(dx: -imageView.bounds.width, dy: 0)
// imageView.frame = imageView.frame.offsetBy(dx: titleLabel.bounds.width, dy: 0)

    }
}复制代码

这里我要多写点东西。由于最开始,我是设置ButtontitleLabelimageViewframe属性的offSet的。

/********** 下面这种作法不推荐 **********/
// 会有问题
titleLabel.frame = titleLabel.frame.offsetBy(dx: -imageView.bounds.width, dy: 0)
imageView.frame = imageView.frame.offsetBy(dx: titleLabel.bounds.width, dy: 0)复制代码

若是按照道理上讲的话,应该是没有什么问题的,titleLabel左移imageView的宽度。imageView右移titleLabel的宽度。但实际上仍是出了问题。运行程序的时候你会发现,箭头图标不见了。

而后我就试着把偏移的距离缩小一倍

竟然就行了,我就很开心。虽然我内心也一直纳闷,为何会是一半的距离!就在我百思不得其解时候,我不当心点击了一下按钮。结果又是令我很是意外

仔细看,箭头图片在文字中央的位置,再屡次点击的话,都是在这个位置切换图片。在这个位置我是能够理解的,由于点击按钮就会执行layoutSubviews方法,就会将titleLabelimageView按照代码里面的偏移量移动,而偏移量又是咱们以前设置的各个宽度的二分之一。

因而我就想到了,若是不设置偏移量是各个宽度的一半的话,最开始显示虽然有问题,可是是否是,点击就正常了呢。果不其然。

因而我测试了强行layoutIfNeeded这种方法也无济于事,我只好参照本身以前用Objctive-C的方法,经过设置titleEdgeInsetsimageEdgeInsets来搞定。

// 将`titleLabel`的`x`向左移动`imageView`的`width`,值得注意的是,这里咱们须要将`width / 2` 
titleEdgeInsets = UIEdgeInsetsMake(0, -imageView.bounds.width, 0, imageView.bounds.width)
// 将`imageView`的`x`向右移动`titleLabel`的`width`,值得注意的是,这里咱们须要将`width / 2`
imageEdgeInsets = UIEdgeInsetsMake(0, titleLabel.bounds.width, 0, -titleLabel.bounds.width)复制代码

这里还有一点我要强调的是,若是只是按照我那样将titleLabelimageView的顺序颠倒的话,titleLabelimageView也是牢牢的挨在一块儿的。大概是下面这个样子

而比较理想的状态应该是,文字与图片之间有必定的间距,这样看起来比较舒服。

若是想达到这种状态,咱们可能会延续上面的思惟,将偏移量增大一点。这种操做表面上看着没什么问题,可是实际上imageView其实已经超出了Button的右侧边界了,显然是不太好的。

// 将`titleLabel`的`x`向左移动`imageView`的`width`,值得注意的是,这里咱们须要将`width / 2`
titleEdgeInsets = UIEdgeInsetsMake(0, -imageView.bounds.width, 0, imageView.bounds.width)
// 将`imageView`的`x`向右移动`titleLabel`的`width`,值得注意的是,这里咱们须要将`width / 2`
imageEdgeInsets = UIEdgeInsetsMake(0, titleLabel.bounds.width + 20, 0, -titleLabel.bounds.width - 20)复制代码

为此,咱们能够尝试转换一种解决思路。给title的文字追加一个空格。

/// 文字在左、图片在右的 Button
class HQTitleButton: UIButton {

    /// 重载构造函数
    ///
    /// - Parameter title: title 若是是 nil,就显示首页
    /// - Parameter title: title 若是不是 nil,显示 title 和 箭头
    init(title: String?) {
        super.init(frame: CGRect())

        if title == nil {
            setTitle("首页", for: .normal)
        } else {
            setTitle(title! + " ", for: .normal)复制代码

这种看起来就比较合适了。


新特性

每次有新的版本的时候,都会出现的一个界面,目的是介绍APP新增的功能之类的。

关于版本号的简单介绍:

  • APP Store每次升级应用程序,版本号都要增长
  • 版本号通常由x.x.x组成,分别对应主版本号.次版本号.修订版本号
  • 主版本号:意味着大的修改,使用者也须要作大的适应,好比Xcode每一年会更新一个主版本号8.3.3
  • 次版本号:意味着小的修改,某些函数和方法的使用或者参数有变化,对应APP多是主功能不变,可是新增了附加的一些新功能
  • 修订版本号:程序内部bug的修订,一些功能的紧急修复,通常不会对APP使用者有任何影响
// MARK: - 新特性
extension HQMainViewController {

    fileprivate func setupNewFeatureView() {

        // 若是用户没有登陆,则不显示新特性界面,直接返回
        if !HQNetWorkManager.shared.userLogon {
            return
        }

        let v = isNewVersion ? HQNewFeatureView() : HQWelcomeView()

        v.frame = view.bounds

        view.addSubview(v)
    }

    /// 计算型属性,不占用存储空间
    fileprivate var isNewVersion: Bool {

        // 获取当前版本号
        let currentVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? ""

        // 拼接保存到沙盒的路径
        let path = String.hq_appendDocmentDirectory(fileName: "version") ?? ""
        let savedVersion = (try? String(contentsOfFile: path)) ?? ""

        // 将当前版本保存到沙盒路径下
        try? currentVersion.write(toFile: path, atomically: true, encoding: .utf8)

        // 比较两个版本是否相同
        return currentVersion != savedVersion
    }
}复制代码

判断新版本这里,可能会有用将版本号转换成数字,而后去逐个对比的作法,我的感受其实不用那么复杂。由于提交到App Store的版本必定是递增的,那么只要比较当前版本和咱们本身保存的版本就彻底能够比对出来的。

给头像作动画处理

准备代码

class HQWelcomeView: UIView {

    fileprivate lazy var backImageView: UIImageView = UIImageView(hq_imageName: "ad_background")
    /// 头像
    fileprivate lazy var avatarImageView: UIImageView = {

        let iv = UIImageView(hq_imageName: "avatar_default_big")
        iv.layer.cornerRadius = 45
        iv.layer.masksToBounds = true
        return iv
    }()
    fileprivate lazy var welcomeLabel: UILabel = {

        let label = UILabel(hq_title: "欢迎归来", fontSize: 18, color: UIColor.hq_titleTextColor)
        label.alpha = 0
        return label
    }()

    override init(frame: CGRect) {
        super.init(frame: frame)

        self.frame = UIScreen.main.bounds

        setupUI()
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}复制代码
// MARK: - UI
extension HQWelcomeView {

    fileprivate func setupUI() {

        addSubview(backImageView)
        addSubview(avatarImageView)
        addSubview(welcomeLabel)

        backImageView.frame = self.bounds
        avatarImageView.snp.makeConstraints { (make) in
            make.bottom.equalTo(self).offset(-200)
            make.centerX.equalTo(self)
            make.width.equalTo(90)
            make.height.equalTo(90)
        }
        welcomeLabel.snp.makeConstraints { (make) in
            make.top.equalTo(avatarImageView.snp.bottom).offset(16)
            make.centerX.equalTo(avatarImageView)
        }
    }
}复制代码

若是这是一个控制器的话,咱们能够选择在viewDidAppear方法里来处理。这里有一个关于自动布局开发的使用原则:

  • 全部使用约束设置位置的控件,不要再设置 frame
    • 缘由:自动布局系统会根据设置的约束,自动计算控件的frame
    • layoutSubviews函数中设置frame
    • 若是咱们主动修改frame,会引发 自动布局系统计算错误!

工做原理:

  • 当有一个运行循环启动,自动布局系统,会收集全部的约束变化
  • 在运行循环结束前,调用layoutSubviews函数统一设置frame
  • 若是但愿某些约束提早更新!使用layoutIfNeeded 函数让自动布局系统,提早更新当前收集到的约束变化

可是咱们这里不是控制器,只是一个View,里面并无viewDidAppear方法。咱们就要找到一个相似的办法。系统提供了一个方法didMoveToWindow,字面上咱们直接能够翻译出它的意思,就是视图被添加到window,表示视图已经显示,和Controller里面的viewDidAppear方法相似。

// MARK: - Animation
extension HQWelcomeView {

    /// 视图被添加到`window`上,表示视图已经显示
    override func didMoveToWindow() {
        super.didMoveToWindow()

        avatarImageView.snp.updateConstraints { (make) in
            make.bottom.equalTo(self).offset(-bounds.size.height + 200)
        }

        UIView.animate(withDuration: 4.0,
                       delay: 0,
                       options: [],
                       animations: { 
                        self.layoutIfNeeded()
        }) { (_) in

        }
    }
}复制代码

通过测试咱们发现,确实能够出现动画了,可是出现的方式有点和咱们所想的不同,咱们是但愿控件已经被建立到咱们以前代码写好的位置,而后再经过动画,移动到下图中最终的位置。该如何处理呢?

上面说自动布局工做原理的时候提到过

  • 若是但愿某些约束提早更新!使用layoutIfNeeded 函数让自动布局系统,提早更新当前收集到的约束变化

所以,咱们手动调用一下layoutIfNeeded方法,将代码布局的约束都建立好,并显示出来,而后再进行更新约束的动画。

// MARK: - Animation
extension HQWelcomeView {

    /// 视图被添加到`window`上,表示视图已经显示
    override func didMoveToWindow() {
        super.didMoveToWindow()

        // 将代码布局的约束都建立好并显示出来,而后再进行下一步的更新动画
        layoutIfNeeded()

        avatarImageView.snp.updateConstraints { (make) in
            make.bottom.equalTo(self).offset(-bounds.size.height + 200)
        }

        UIView.animate(withDuration: 2.0,
                       delay: 0,
                       usingSpringWithDamping: 0.7,
                       initialSpringVelocity: 0,
                       options: [],
                       animations: { 
                        self.layoutIfNeeded()
        }) { (_) in

            UIView.animate(withDuration: 1.0,
                           animations: { 
                            self.welcomeLabel.alpha = 1
            }, completion: { (_) in
                self.removeFromSuperview()
            })
        }
    }
}复制代码

设置头像

UI布局完毕之后,就剩下将头像设置到上面了,通常来说这些都是没什么技术含量的。可是这里我仍是想简单介绍一下。

我这里仍是将设置头像的代码放在了didMoveToWindowlayoutIfNeeded方法后面去执行,

这里须要提醒的是,若是是纯代码开发,不会走这个方法,即使是这段话仍然须要加上,可是若是你在init?(coder aDecoder: NSCoder)中写代码,会提示你Will never be executed

并且即使是xib开发,这里也仅仅是将xib的二进制文件将视图数据加载完成,尚未和代码连线创建起关系,因此开发时,不能在这个方法里面处理UI,并且若是是xib开发的话,你打印视图的话,结果都是nil的。

/// 设置头像
fileprivate func setAvatar() {

    guard let urlString = HQNetWorkManager.shared.userAccount.avatar_large else {
        return
    }
    avatarImageView.hq_setImage(urlString: urlString, placeholderImage: UIImage(named: "avatar_default_big"))
}复制代码

新特性界面

因为咱们以前在HQMainViewController中作好了判断是显示新特性界面仍是显示欢迎界面。所以,咱们处理好欢迎界面之后,就仿照相似的方法建立新特性界面就行了。

// MARK: - 新特性
extension HQMainViewController {

    fileprivate func setupNewFeatureView() {

        // 若是用户没有登陆,则不显示新特性界面,直接返回
        if !HQNetWorkManager.shared.userLogon {
            return
        }

        let v = isNewVersion ? HQNewFeatureView() : HQWelcomeView()复制代码

HQNewFeatureView中,进行布局,我写UI布局套路都比较单一,懒加载控件,在extensionsetupUI,若是有按钮的监听方法,再将按钮的监听方法抽取到extension中,只是暂时我本身习惯这样写而已。

class HQNewFeatureView: UIView {

    /// 开始体验按钮
    fileprivate lazy var startButton: UIButton = UIButton(hq_title: "开始体验", color: UIColor.white, backImageName: "new_feature_finish_button")
    /// pageControl
    fileprivate lazy var pageControl: UIPageControl = {
        let pageControl = UIPageControl()
        pageControl.numberOfPages = 4
        pageControl.currentPageIndicatorTintColor = UIColor.orange
        pageControl.pageIndicatorTintColor = UIColor.black
        return pageControl
    }()
    fileprivate lazy var scrollView: UIScrollView = {
        let scrollView = UIScrollView(frame: UIScreen.main.bounds)
        return scrollView
    }()

    override init(frame: CGRect) {
        super.init(frame: frame)

        self.frame = UIScreen.main.bounds

        setupUI()
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}复制代码
// MARK: - UI
extension HQNewFeatureView {

    /// setupUI
    fileprivate func setupUI() {

        addSubview(scrollView)
        addSubview(startButton)
        addSubview(pageControl)

        startButton.isHidden = true
        startButton.addTarget(self, action: #selector(enter), for: .touchUpInside)

        setupScrollView()

        startButton.snp.makeConstraints { (make) in
            make.centerX.equalTo(self)
            make.bottom.equalTo(self).multipliedBy(0.7)
        }
        pageControl.snp.makeConstraints { (make) in
            make.centerX.equalTo(startButton)
            make.top.equalTo(startButton.snp.bottom).offset(16)
        }
    }

    /// setupImageViewFrame
    fileprivate func setupScrollView() {

        let count = 4
        let rect = UIScreen.main.bounds

        for i in 0..<count {

            let imageName = "new_feature_\(i + 1)"
            let iv = UIImageView(hq_imageName: imageName)

            iv.frame = rect.offsetBy(dx: CGFloat(i) * rect.width, dy: 0)
            scrollView.addSubview(iv)
        }

        /// 设置`scrollView`的属性
        // 这里加`1`是为了让`scrollView`能够多滚动一屏
        scrollView.contentSize = CGSize(width: CGFloat(count + 1) * rect.width, height: rect.height)
        scrollView.bounces = false
        scrollView.isPagingEnabled = true
        scrollView.showsHorizontalScrollIndicator = false
        scrollView.showsVerticalScrollIndicator = false

    }
}复制代码
// MARK: - Target Action
extension HQNewFeatureView {

    @objc fileprivate func enter() {
        print("enter")
    }
}复制代码

界面布局完毕之后,剩下的就是完善其它的业务逻辑了。主要还得靠scrollViewdelegate去实现

// MARK: - UIScrollViewDelegate
extension HQNewFeatureView: UIScrollViewDelegate {

    func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {

        // 滚动到最后一个空白页面,将新特性页面从父视图移除
        let page = Int(scrollView.contentOffset.x / scrollView.bounds.width)

        if page == scrollView.subviews.count {
            removeFromSuperview()
        }
        // 若是不是倒数第二页,那么就隐藏`startButton`按钮
        startButton.isHidden = (page != scrollView.subviews.count - 1)
    }

    func scrollViewDidScroll(_ scrollView: UIScrollView) {

        // 一旦滚动,隐藏按钮
        startButton.isHidden = true

        // 设置当前的偏移量,+0.5是为了处理`scrollView`滚动超过屏幕一半的时候,`pageControl`也滚动到下一页
        let page = Int(scrollView.contentOffset.x / scrollView.bounds.width + 0.5)

        // 设置分页控件
        pageControl.currentPage = page

        // 分页控件的隐藏,滚动到最后一页的时候
        pageControl.isHidden = (page == scrollView.subviews.count)
    }
}复制代码
// MARK: - Target Action
extension HQNewFeatureView {

    @objc fileprivate func enter() {
        removeFromSuperview()
    }
}复制代码

效果以下图所示

至此为止,总体框架基本搭建完毕,下一篇介绍自定义微博的cell及体会MVVM的好处。

DEMO传送门:HQSwiftMVVM

欢迎来个人简书看看:红鲤鱼与绿鲤鱼与驴___

相关文章
相关标签/搜索