肇鑫的技术博客

业精于勤,荒于嬉

UISplitViewController的几种模式

模型

  • UISplitViewController拥有两个视图控制器,在故事版中分别对应主视图控制器和细节视图控制器,在编码中对应为viewControllers属性,主视图控制器在前,细节视图控制器在后。
  • 开发者通过preferredDisplayMode推荐想要的模式,通过displayMode获得当前的模式。
  • 在iPhone中,横屏、和竖屏都是allVisible,此时可以通过UISplitViewControllerDelegatesplitViewController(_:collapseSecondary:onto:)方法来告诉系统默认是显示哪个视图控制器。
  • 在iPad中,横屏是allVisible,竖屏是primaryHidden。此时如果想要默认显示主视图控制器,需要在UISplitViewControllerDelegatesplitViewController(_:willChangeTo:)方法,设定preferredDisplayModeprimaryOverlay

UISplitViewController

  • displayMode
    • 这是一个只读属性。
    • 它代表的是UISplitViewController当前的具体模式。
  • preferredDisplayMode
    • 开发者只能通过displayMode了解到UISplitViewController当前的模式。如果开发者不满意这个模式,想要手动调节,就可以通过修改preferredDisplayMode来实现。
    • 值得注意的是,这个是开发者的推荐,并不是完全的设定,系统仅会在条件允许的情况下,优先使用这个推荐。如果系统认为可视面积过小,就会忽略这个推荐,而使用系统认为最适合的模式。

UISplitViewController.DisplayMode(按照rawValue的数值排列)

  • automatic
    • 自动模式是默认的模式,完全由系统进行判断,开发者没有任何推荐。
    • 注意:这个自动模式,只在preferredDisplayMode中有效,displayMode实际显示的,只会是下面三个模式。
  • primaryHidden
    • UISplitViewController包含两个视图控制器,主视图控制器和细节视图控制器。primaryHidden表示优先显示细节视图控制器,而将主视图控制器隐藏起来。用户需要使用手势或者是后退按钮显示主视图控制器。
    • iPad在竖屏时默认会是这个模式。
  • allVisible
    • 同时显示主视图控制器和细节视图控制器。
    • 需要注意:这个模式虽然叫allVisible,却不是一定都能同时显示。如果不能,就会需要告诉系统,优先显示哪个视图控制器。
    • iPad在横屏时默认会是这个模式。此时会全部显示。
    • iPhone 6s Plus在竖屏和横屏时都是这个模式。竖屏时,因为实际上不能全部显示,就还需要考虑UISplitViewControllerDelegatesplitViewController(_:collapseSecondary:onto:)方法。
  • primaryOverlay
    • primaryHidden类似。但是会优先显示主视图控制器,叠加在细节视图控制器之上。
    • 如果我们要特别提示用户主视图控制器的存在,就可以使用这个选项。

UISplitViewControllerDelegate

splitViewController(_:collapseSecondary:onto:)方法

UISplitViewController.DisplayModeallVisible,且可显示的面积不能同时容纳显示主视图控制器和细节视图控制器时,系统会调用UISplitViewControllerDelegatesplitViewController(_:collapseSecondary:onto:)方法。

这个方法返回true代表显示主视图控制器,返回false代表显示细节视图控制器。

splitViewController(_:willChangeTo:)方法

当你需要在系统选择了primaryHidden时,就更改为primaryOverlay。使用此方法。

iOS自定义AlertController

最初的代码在这里。作者自己的说明

最初的作品已经很好用了。只是细节需要改动一下。

更改

  1. 原代码低于Swift 4.0,Xcode 11 beta 5无法编译。此次,先将Swift改成4.0。发现一切正常,没有需要改动的地方。
  2. 原代码不支持暗模式,可以在故事版中将颜色重新选择为支持暗模式的。特别的,在资源中新建颜色资源AlertBackgroundColor,获得模拟器中UIAlertController.view在亮/暗模式下的背景颜色。并指定为Alert View的背景色。
  3. 原代码ViewController.swift第55行使用了.overCurrentContext,这个在master/detail模式的布局中,如果detail中弹出自定义的警告窗口,之后旋转屏幕或者更改显示模式(比如由亮改暗),警告窗口就会出现问题。此处需要改成.overFullScreen
  4. 原代码CustomAlertView.swift第23行alertViewGrayColor使用了自定义颜色,注释掉掉。在第35行插入let alertViewGrayColor = UIColor.systemGray2
  5. CustomAlertView.swift第36行插入let lineThickness:CGFloat = 0.5,并且将第37-39行的width: 1.0改成width: lineThickness

小结

经过以上的改动,就可以获得一个支持iOS 13 beta的自定义AlertController了。剩下的你只需要根据自己的需求,继续填充就可以了。

Realm模型升级(二)

Realm模型升级(一)中讲了对象添加了新属性之后要如何升级。最近我又遇到了新的问题,要将对象改名。

新问题

因为业务的需要,必须将代码

class Item:Object {
    static let calendar = Calendar(identifier: .gregorian)
    
    @objc dynamic var title = ""
    @objc dynamic var addedDate:Date = Date()
    @objc dynamic var relatedItem:Item? = nil
    let records = List<Record>()
    
    override class func primaryKey() -> String? {
        return "title"
    }
}

class Record:Object {
    static let dateFormatter:DateFormatter = {
        let df = DateFormatter()
        df.locale = Locale(identifier: "zh")
        df.dateStyle = .medium
        df.timeStyle = .none
        
        return df
    }()
    
    @objc dynamic var id:Int = 1
    @objc dynamic var addedDate:Date = Date()
}

更改为

class ACItem:Object {
    static let calendar = Calendar(identifier: .gregorian)
    
    @objc dynamic var title = ""
    @objc dynamic var addedDate:Date = Date()
    @objc dynamic var relatedItem:ACItem? = nil
    let records = List<ACRecord>()
    
    override class func primaryKey() -> String? {
        return "title"
    }
}

class ACRecord:Object {
    static let dateFormatter:DateFormatter = {
        let df = DateFormatter()
        df.locale = Locale(identifier: "zh")
        df.dateStyle = .medium
        df.timeStyle = .none
        
        return df
    }()
    
    @objc dynamic var id:Int = 1
    @objc dynamic var addedDate:Date = Date()
}

即原来的类的名称之前,需要添加AC字样。更改了代码之后运行,发现数据不见了。

分析

打开数据库文件查看(如图)。原来,虽然代码中的类变了,但是数据库中的类还是原来的名字,需要手动迁移。

realm database

解决方案

23-39行,利用之前的Item对象,生成ACItem对象;利用Item对象中的records属性,生成ACRecord对象。需要注意的事,因为代码中此时已经没有了Item类和Record类,我们没法使用Item.className(),而只能直接使用"Item"

41-43行,删除掉旧数据。

let config:Realm.Configuration = {
    // update realm
    let config = Realm.Configuration(
        fileURL: destinationURL,
        
        // Set the new schema version. This must be greater than the previously used
        // version (if you've never set a schema version before, the version is 0).
        schemaVersion: 3,
        
        // Set the block which will be called automatically when opening a Realm with
        // a schema version lower than the one set above
        migrationBlock: { migration, oldSchemaVersion in
            // We haven’t migrated anything yet, so oldSchemaVersion == 0
            if (oldSchemaVersion < 1) {
                // Nothing to do!
            }
            
            if (oldSchemaVersion < 2) {
                // Nothing to do!
            }
            
            if (oldSchemaVersion < 3) {
                migration.enumerateObjects(ofType: "Item") { (oldObject, _) in
                    let acItem = migration.create(ACItem.className())
                    acItem["title"] = oldObject!["title"]
                    acItem["addedDate"] = oldObject!["addedDate"]
                    
                    guard let list = acItem["records"] as? List<MigrationObject>,
                        let oldList = oldObject!["records"] as? List<MigrationObject> else {
                        fatalError()
                    }
                    
                    oldList.forEach { o in
                        let acRecord = migration.create(ACRecord.className())
                        acRecord["id"] = o["id"]
                        acRecord["addedDate"] = o["addedDate"]
                        list.append(acRecord)
                    }
                }
                
                // delete
                migration.deleteData(forType: "Item")
                migration.deleteData(forType: "Record")
            }
    })
    
    return config
}()

总结

  1. 代码中对象的改名,意味着数据库中数据的迁移。
  2. MigrationObjectEnumerateBlock中的oldObject,实际上一个词典,我们通过键来进行数据的查找。