中介者模式(Mediator Pattern)是用来降低多个对象和类之间的通信复杂性。这种模式提供了一个中介类,该类通常处理不同类之间的通信,并支持松耦合,使代码易于维护。中介者模式属于行为型模式。
在MVC 框架中,其中C(控制器)就是 M(模型)和 V(视图)的中介者。我们接下来用一个搜索视图来展示中介者模式:

有限状态自动机(FSM “finite state machine” 或者FSA “finite state automaton” )是为研究有限内存的计算过程和某些语言类而抽象出的一种计算模型。有限状态自动机拥有有限数量的状态,每个状态可以迁移到零个或多个状态,输入字串决定执行哪个状态的迁移。有限状态自动机可以表示为一个有向图。有限状态自动机是自动机理论的研究对象。
我们来看以下片段代码:

1
2
3
4
5
6
7
class ViewController {
@IBOutlet var tableView: UITableView!
@IBOutlet var errorLabel: UILabel!
@IBOutlet var emptyStateLabel: UILabel!
@IBOutlet var activityIndicator: UIActivityIndicatorView!
// Some implementation
}

我们得出4种状态,且每种独立存在:

  • 显示数据
  • 是否在加载数据中
  • 是否显示错误
  • 空状态
    我们在控制器中定义该枚举:
    1
    2
    3
    4
    5
    6
    7
    8
    extension ViewController {
    enum State {
    case loading
    case showingData([Item])
    case empty
    case error(Error)
    }
    }

Swift能实现多继承吗?

虽然多继承在C++中可以实现,但在Swift中,是不能够实现多继承的。但一个class可以遵循多个协议,只能继承一个父类。但值类型比如 struct , enum 就不存在继承这么一说,但还是可以遵循多个协议的。
Swift只支持协议多继承
如:

1
2
3
4
5
6
7
protocol ChildAB:ChildA,ChildB {

}
class MyClass: ChildAB {
func method() {
}
}

Swift与OC最大的区别就是新增了值类型Struct, let变量修饰后无法再次被赋值实现真正的immutable,也就是说一旦赋值不能再次修改。虽然OC中定义NSArray *arr 也是一个immutable,但是arr对象指针指向还可以被再次重新赋值。OC是一门运行时语言,arr所指向的变量都是运行时获取对象。OC中使用immutable不直接等同于线程安全。Objective-C中存在深拷贝、浅拷贝,即使你调用一个[NSMutableArray Copy]得到的NSArray也不代表这个数组中的对象都是经过深拷贝。

我们先看一下如下代码会造成线程安全问题?:

1
2
3
if (self.xxx) {
[self.dict setObject:@"ah" forKey:self.xxx];
}

在单线程模式下肯定是没啥问题,但在多线程情况下比如线程A,线程B两者都同时进入了if条件,当A正要执行set操作,B已经对self.dict = nil。那么线程A肯定是不能设置成功的。
还有另外一种情况,当我们的数据资源比如集合,多线程对资源进行争夺。如添加和移除操作。为了保证最终结果的准确性,我们通常会给操作代码加锁。
1
2
3
OSSpinLock(&_lock);
[self.array addObject:@"data1"];
OSSpinUnlock(&_lock);

我们都想写得一手好代码。这包含防止内存泄露,当在写闭包时业务如果需要用到self,我们会使用[weak self] 修饰。但为什么需要弱捕获呢?是否任何情况下在闭包里都要用weak? 接下来我们将用捕获列表来回答这几个问题。我们将讲解到不同的捕获,以及什么时候用合适的捕获去处理问题。

什么是捕获列表

我们先看以下代码片段,调用闭包后显示的结果:

1
2
3
4
5
6
7
8
var name = "qiu"
var appendToName = { (string: String) -> String in
return name.appending(string)
}

let one = appendToName("gaoying") //qiugaoying
name = "liu"
let two = appendToName("gaoying") //liugaoying

闭包可对同一作用域范围内的属性做引用,也就是闭包内持有的属性在同一上下文作用域内变更受影响。我们再观察以下代码:

1
2
3
4
5
6
7
8
var name = "qiu"
var appendToName = { [name] (string: String) -> String in
return name.appending(string)
}

let one = appendToName("gaoying") //qiugaoying
name = "liu"
let two = appendToName("gaoying") //qiugaoying

这时输出的结果都为qiugaoying。[name] 将属性name放在闭包的捕获列表,明确地告诉闭包强持有闭包首次调用时的name值。如果要在闭包捕获列表内捕获多个值,则用逗号隔开,如[property, anotherProperty] in 方式。

在我们设计网络层响应结果的时候,Result是大家用的比较多的。和Optional有点类似,它是用来表述返回类型是成功或者失败。在Swift5.0中,Swift团队已经把Result加入到了标准库。这篇文章将讲解到何时去使用Result,以及怎样使用Result。
上篇文章,我们讲到Swift异常处理,在处理时候用do{}catch{}捕捉异常,以及在方法名后通过throws关键字将异常往上层抛。和抛错误不同的是,在代码中用Result作为返回类型可以达到我们需要的效果。更大的不同是,Result可以用于在异步处理时候,如鱼得水般地优雅处理成功与失败的回调返回。在讲解之前,我们先看一下以下片段1代码:

1
2
3
4
5
6
7
8
9
10
func loadData(from url: URL, completion:@escaping (Data?) -> Void) throws {
URLSession.shared.dataTask(with: url) { data, response, error in
if let error = error {
throw error
}
if let data = data {
completion(data)
}
}.resume()
}

按照以往的方式,当error有异常时候,我们进行抛出异常,然后方法上也加上throws。假设以上代码编译通过,那么在调用的时候代码片段如下:
1
2
3
4
5
6
7
8
9
do {
try loadData(from: aURL) { data in
print("fetched data")
}

print("This will be executed before any data is fetched from the network.") //这行代码会被先执行。由于loadData方法是异步处理请求数据,就算发生错误,error抛出存在一定的执行时长。这就违背了异常捕捉而中止的性质了,也谈不上异常捕捉。
} catch {
print(error)
}

在swift2.0, 苹果在swift中就介绍了throws 关键字。这方便开发者在写代码时候保证清晰的思路以及在执行时可能出现的错误做异常处理。可做异常捕捉或者忽略异常。这篇文章将详细讲解 throws 关键字,以及在代码中如何去处理异常。

处理异常

如果你在Swift中使用过JSONDecoder,就知道在编写代码时候可抛出异常。让我们来看以下代码片段:

1
2
3
4
5
6
7
8
9
var greeting = "Hello, playground"

do {
let data = greeting.data(using: .utf8)!
let decoder = JSONDecoder()
let string = try decoder.decode(String.self, from: data)
} catch { error
print(error)
}

在Playground 执行代码片段,发现控制台输出Swift.DecodingError异常错误,异常被catch捕捉到了。代码中,我们用do {} catch {} 方式把可能发生的代码一次用try 方式写在do{}里面。当捕捉到一次立马进入catch{error }段。catch将捕捉到Error, 在catch时候,我们也可以捕捉精确的异常进行处理。

在App开发中,数据层往往会做本地缓存,以便在网络不好的情况下能够先显示本地缓存数据。接下来将讲解到如何设计本地数据源中间件来实现业务数据的读取。在讲解之前我们会先了解一下简单泛型在Swift中的应用,结合associated types, 你也可以在协议中使用泛型。

在介绍之前,我刚提到我想要对泛型数据存储在本地,然后还需要一个远程数据接口API提供数据以便在本地数据没有的情况下调用。假设我需要用户信息数据:

1
2
3
4
5
6
7
let localDataStore = UserDataStore()
let remoteDataStore = UserApi()
let dataStore = CacheBackedDataStore(localDataStore, remoteDataStore)

dataStore.fetch(userID) { result in
// handle result
}

简介

文章将简要讲述何时使用@State, @Binding, @StateObject, @ObservedObject, @EnvironmentObject。

@State

@State属性包裹器可用于当你的视图对象响应任何状态改变时候。换句话说,视图初始化@State修饰的属性变量。该属性变量的变更只发生在内部View,而不被外界所改变。

1
2
3
4
5
6
7
8
9
10
11
12
13
struct StateExample: View {
@State private var intValue = 0

var body: some View {
VStack {
Text("intValue equals \(intValue)")

Button("Increment") {
intValue += 1
}
}
}
}

在内部,SwiftUI将会存储 @State修饰的属性值,然后贯穿整个渲染或重复渲染的生命周期中将持久化存储这个值,当在视图刷新或者重新创建的时候,该属性值可以很好地被视图自身管理。当然,你也可以用private修饰词修饰@State属性,保证只在内部修改@State属性。
@State使用场景:

  • 在被修饰的属性需要响应你视图变化的状态时候
  • 你修饰的是一个值类型(结构体、枚举)

大多数产品在本地Sqlite存储的时候,基本都是用了串行模式。即整个应用全局只有一个sql操作句柄,用单例管理着这个SqlHandler。另外还有一个模式叫线程模式。这种模式下很容易出现Sqlite_Busy错误。

  1. 当有写操作时,其他读操作会被驳回。
  2. 当有写操作时,其他写操作会被驳回。
  3. 当开启事务时,在事务提交之前,其他写操作会被驳回。
  4. 当开启事务时,在事务提交之前,其他事务请求会被驳回。
  5. 当有读操作时,其他写操作会被驳回。
  6. 读操作之间能够并发执行。

第三方库WCDB支持多线程度于读与读,读与写并发执行,写与写串行执行。WCDB 在多线程方面明显优于 FMDB 和 ModelSqliteKit,通过 WCDB 的改造,使得SQLite的性能发挥到极致。

简介

图片编辑是基于 CLImageEditor 第三方进行改装得到仿微信的图片编辑交互。原CLImageEditor库是一个日本人写的图片编辑,支持涂鸦,裁剪,旋转,文字帖功能。但有几项我们进行改进。

  • 所有的这些图片操作我们汇总到一起进行编辑,最终保存的时候才处理所有操作汇总成一张处理图片。
  • 涂鸦撤销功能实现真正的上一步撤销,而不是仅仅透明色进行橡皮擦擦除。
  • 新增马赛克操作功能。
  • 把图片裁剪和旋转进行结合到一个页面操作,另外新增裁剪、旋转之后的图片恢复。
  • 改变颜色选择,文字编辑的交互,新增图片裁剪区域的边角重点标注。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    CLImageEditorViewController

    usedToolDic:使用过的工具【toolName:CLImageToolBase

    -(CLImageToolBase *)currentEditTool; 当前正在使用的工具。

    -(UIImage *)getMergeDrawImageForCropTool; 最后合成的图片。用于业务转发、保存、回调给用户。

    //图片编辑器所支持的工具类都继承于CLImageToolBase, 通过CLClassList辅助类runtime方式自动将相关工具类注入到菜单项中。
    + (NSArray*)subtools
    {
    return [CLImageToolInfo toolsWithToolClass:[CLImageToolBase class]];
    }
    CLImageToolBase 工具基类:持有editor编辑器方便拿到图片,同时具有setup, cleanUp 这几个“生命周期”方法,做相关工具的视图创建与清除。

我们每个人的手机都安装有微信App,肯定大家都使用过以下场景在聊天群中,当有新的成员进入的时候,可以看到 某某邀请了某某1进入群聊,或者当你是群主的时候,某管理人员邀请了几个人进入时候看到 某某管理员邀请了张三、李四,王五,张三(同名)进入了群聊。对于邀请人员或者被邀请人员,我们发现人员的名称是可以点击的,即使同名的人,点击其名字也可以跳转到各自的主页详情中。

如何存储人员的id?

一条群成员变更通知就是一串文本,我们点击的时候肯定要存储好他的人员id,然后点击的时候就能拿到对应人员的id。从最简单的存储方案来看,这条内容可以写成以下格式: 管理员刘某某邀请了张三李四王五张三进入了群聊。换成代码的格式即为:“管理员\”刘某某::1000\”邀请了\”张三::1001、李四::1002、王五::1003、张三::1004\”进入了群聊”。其中的代码格式为userName::userId这样一种形式进行拼接,中间的符号可以采用其他特殊符号代替。这里为了区别用户特殊性输入用了双冒号。多个成员之间用顿号隔开,人员与普通内容之间用符号双引号\”进行分割(斜杆为转义符)。 我们需要对这一串文字表达式进行解析进行UI显示,把人员名字后的拼接符号用户id进行过滤掉,组合成不带用户id的易懂文字内容给到用户。

开发中由于系统版本以及sdk,第三方库等原因,都会影响成出现一些奇怪的问题。当时是引用YYTextview控件解决消息cell长文本的光标选择范围内容复制的小功能。由于YYTextview光标弹出层自定义在另外一个新的Windown上,然后左侧侧滑出的view是新增在另一个新的Windown中。当侧边栏dismiss的时候,可能由于windown层级的影响,当在聊天输入框再次输入文字长按时候,系统出现的UIMenu层的Button不见了。当时想从根本问题去排查为啥系统的UIMenu当长按的时候操作功能的button为啥不见去解决,花费了将近2个多小时通过Xcode调试查看层级,阅读YYTextview的光标自定义层的实现以及添加到windown的层级,查看左侧侧边栏添加动画实现方式等都未解决。然后想到如果不通过新创建window来添加view 实现动画,而是通过present一个控制器实现侧边栏动画。最终问题得到了解决。

旧Slide侧滑方案

旧有的侧滑方案是创建一个新window,然后设置其windowLevel 为正常的+1,这样就在最顶层了,然后设置其rootViewController为侧滑的控制器。很有可能是新创建的window与YYtextView的光标选择层创建的window有冲突,然后造成UIMenu按钮消失了。

改进方案,Present一个控制器 SlideVC

实则我们可以通过拿到最顶层控制器topVC,用导航控制器把SlideVC包裹好,设置它的 modalPresentationStyle 模式为UIModalPresentationOverCurrentContext,然后present出来 .这里要注意的是,系统的present方法是否有被runtime method swizled进行方法替换来公共统一处理modelPresentStyle问题,因为有些App交互不想使用系统的抽屉式present效果, 如果有需要做相关的过滤业务控制器逻辑判断。 侧边动画的实现:在SlideVC中定义一个半透明层背景bgView,添加tap手势,点击即关闭。往bgView添加一个左侧内容视图leftView 75%宽,右侧rightView。self.view添加一个Pan手势,用来计算leftView拖拽时候X的值位移变化。将内容控制器交由SlideVC管理:

2020 年下半年iOS工作技术点 tips

背景:疫情原因导致之前云喵工作不能够继续,2020年6月还是面试找了一份相对稳定的工作,去了南航的一家外包供应商。

E App 是一个企业内部OA即时通讯App. 驻场南航后主要是针对此项目进行功能迭代敏捷开发。主要说一下几个技术任务点:

  1. 首要任务就是将旧有MRC内存运行环境改成ARC运行环境。
    任务到不是复杂,主要修改点是多,体力劳动。将ARC相关的修饰关键词,控制器的dealloc 方法以及设置的代理delegate, 配置文件-fno-objc 相关配置干掉。
    遇到的问题?
    期间由于之前是MRC手动内存回收机制,由于开发人员代码某些变量没有回收或者泄露贮存在内存中,当改成ARC之后会自动释放导致再次使用该变量的时候出现nil值崩溃或者业务的中断开!
    C++代码块Client类某方法的变量被释放造成崩溃?
    当时问题点抛向如何停止某线程的解决方案上去了,而不是终止某条件然后让线程自然停止。当程序进入后台applicationWillTerminate的时候,手动退出IM.然后进入销毁连接阶段。为了使alive_thread , recv_thread 两线程退出,在CLIENT_Disconnect 销毁阶段,通过改变alivethread的条件让while条件中止,线程必然退出。
  2. h5网页打卡有时候定位不到或者崩溃。因App程序使用了百度地图sdk,所以直接升级百度地图sdk即可。sdk编译分xcode11.3 或 xcode 12 。具体看E项目如果采取xcode11.3 编译打包上线,那么需要找到适应的.a包或者framework 进行编译。
  3. 将ASI网络文件下载改成AFNetwroking网络文件下载,将长进度条改成扇形进度条。
    ASI那套网络请求比较老旧,当时还是用NSURLConnnection来进行网络请求,而且需要单独维护一个网络线程长期贮存到内存中。AFN 采取NSURLSession来进行网络请求,线程即用即回收。
    AFN请求时设置支持SSL安全策略。

    1
    2
    3
    4
    5
    AFSecurityPolicy *securityPolicy = [[AFSecurityPolicy alloc] init];
    securityPolicy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModeNone];
    securityPolicy.allowInvalidCertificates = YES;
    securityPolicy.validatesDomainName = NO;
    [_manager setSecurityPolicy:securityPolicy];

    然后通过NSURLSessionDownloadTask 进行下载request,返回对应的进度,通过KVO进行进度条的UI显示,也可通过Block传递到控制器进行显示。