肇鑫的技术博客

业精于勤,荒于嬉

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,实际上一个词典,我们通过键来进行数据的查找。

咕唧在iPad浮屏时,有时会意外地重新加载账户头像问题的处理

咕唧在iPad浮屏时,既屏幕右侧向里滑动弹出咕唧,有时会出现全部头像闪动一下的问题。这个问题没什么规律,如果立即你把它划回去,再重新滑出来,它就又没有这个问题了。另外,我在iOS 12的时候也没有遇到过这个问题。

我认为这可能是iOS 13 beta独有的一个问题。

我尝试解决这个问题。

方案一

因为视图是UITableView,我尝试减少reloadData()的次数。仔细阅读代码,修改了几处限定条件,减少了reloadData()。并且出于调试目的,我在预期不应该发生的位置,插入fatalError()强制结束应用。

测试结果

方案一减少了重新加载表格视图的次数,但是对于解决该问题无效。测试中发现,偶尔还是会出现重新加载头像的问题,并且,应用没有崩溃,也就是说,我预期不会执行的部分,也的确没有执行。

方案二

因为问题是和头像有关,于是着重查看头像部分的代码。

cell.imageView?.image = profileImage
            
DispatchQueue.main.async {
    ProfileImageHelper.decorate(cell.imageView!, for: accountCore.type)
}

这段代码使用了异步代码,在UI队列里执行了对于UIImageView的装饰。这里必须使用的原因是cell.imageView是只读属性,不能直接替代。并且,当cell.imageView?.image = profileImage之后,如果立即装饰图片视图,图片视图的尺寸还是0,必须在下一个事件循环才能获得实际尺寸。

imageView_decoration

上图是咕唧账户头像的部分截图。照片原本是正方形,在实际显示时,被处理为圆形。并且,根据账户类型的不同,加上了颜色不同的圆圈。

我们知道,异步的代码,与同步不同,是分成两次执行的。因此,存在一种可能,既第一部分的代码执行了之后,很久,第二部分才执行。这在显示上,就会出现先显示方形的头像,然后才变成圆形和圆圈,就像看幻灯片那样。

须要将此部分的代码由异步改为同步。

此前采用异步的原因是无法立即知道图片视图的大小,改成同步,就必须修改代码,手动指定视图的大小。我们知道,这在平时其实是一种不好的行为。代码写死了,就失去了灵活性。但是这里,我们不得不把它写死。

通过调试,我们知道这里目前的大小是48.0点。(点,point,是苹果使用的相对大小的尺寸单位)。

cell.imageView?.image = profileImage
ProfileImageHelper.decorate(cell.imageView!, for: accountCore.type)

测试结果

这次的测试很成功,之后没有发现再次出现这个问题。

方案三

虽然问题解决了,但是写死代码还是不够灵活。思考了一下,虽然图片视图的尺寸不能立即知道,但是图片的尺寸是可以知道的。因此,将写死的代码,改为使用图片的尺寸。这样问题就彻底解决了。

思考

虽然这个问题很有可能是iOS 13 beta引入的,但是解决掉这个问题,还是有收获的。我们在实际编程中,应该尽量减少异步代码的使用。因为,有时,异步代码会带来意想不到的问题。