移动开发中实现 Deep Linking 的 URL Scheme 和 Universal Links 的区别是什么?

经历过的 Mobile 项目基本上都有支持 Deep Linking 的需求,每次新项目都会经历向其他端同事和 BA 解释实现 Deep Linking 两种方案的区别,于是就有了这一篇短文。主要是介绍两种方案的优缺点和实现成本差异,并不涉及如何实现的代码步骤。 首先什么是 Deep Linking,简单来说就是让一个 App 可以通过 URL 打开其他的 App,以提供更便捷高效的用户体验。 如果想跳到别人那去 BA 老刘:「如果想在 App 里打开人家的 App 应该怎么做呢?」 开发小曾:「目前有两种选项,URL Scheme 和 Universal Links。」 BA 老刘:「区别是啥?」 开发小曾:「主要看你想不想处理用户没有安装对方 App 的情况。」 ps: Android 中这两种选项是 Deep Links 和 App Links,运作原理大体相同,实现方式 iOS 和 Android 有些许差异。为少打字,下文将统一使用 URL Scheme 和 Universal Links。 URL Scheme 通常长这样: example://destination?param1=hello。 想通过 URL Scheme 跳转到某个 App,我们需要知道对方 App 定义的 Scheme 是什么(这不算是废话)。 以 Twitter 为例,如果当前设备安装了 Twitter: 通过 twitter://user?screen_name=elonmusk 这样一个 URL Scheme,就可以打开 Twitter 并且跳转到 Elon Musk 的主页(scheme 可以输入 Safari 地址栏进行测试)。 ...

July 27, 2022

参加 Apple 开发者线上活动是什么样的体验?

从朋友圈看到思琦发了一个《使用 SwiftUI 打造卓越体验》的 Apple 开发者线上活动的报名链接,刚好最近参与的项目也在大规模使用 SwiftUI 就报名了,即使时间很不凑巧是工作日(另一报名要求是必须要有中国区的开发者账号)。 活动分两天第一天主要是一些 SwiftUI 的介绍,SwiftUI 对数据的处理和布局的一些要点。第二天是社区开发者交流。 技术细节后面可以分好几篇博客来描述,但总的来说技术相关的收获其实和自己去看 WWDC 差不多。并且由于长期以 2x 速度观看这类视频,刚开始还出现了一些不适应。但想着这是来自苹果的开发者的分享,还是管住了想拿起手机的手,毕竟机会难得。 事实证明确实认真观看还是有收获,主要是以下几点: 知道了一些 SwiftUI 相关的 Xcode 快捷操作,比如 Preview 和代码其实是可以相互影响的。Library 中可以搜索 ViewModifier 等等。 来自苹果的工程师对 SwiftUI 这样响应式 UI 编写方式的数据流思考的建议。 一些编写 SwiftUI 代码时能够让渲染引擎更高效的建议。 除了上面开发者分享的内容之外,最重要的是可以提出一些自己在使用 SwiftUI 时遇到的问题,感觉只要描述得够清楚并且和本次活动主题相关,那么都能够得到苹果开发者的解答,虽然受形式或时间限制,答案并不一定完整,但当一个 SwiftUI 的疑问是由苹果的工程师解答时,即使只是关键字的指引也能起到画龙点睛的作用(比如 StateObject 与 ObservedObject 的差异)。 第二天更多的是听社区开发者的一些交流,收获到了不少之前并不知道的资料。一并列在下方。 ps:感觉参加这次活动对自己来说还有一个副作用就是,由于资料的推荐者有足够的可信度,并且提供了一个可靠的学习路线,所以会用格外的认真来对待它们。 Swift UI 的入门指引 WWDC - Introduction to SwiftUI WWDC - SwiftUI Essentials 十个很有代表性的 Playground 交互式的 SwiftUI 入门教程 使用 SwiftUI 构建完整 App 的交互式教程 SwiftUI 数据处理要素 ...

March 28, 2022

iOS 架构之另一种依赖注入的思路

在 iOS 业务开发过程经常面对网络请求,数据持久化这样带有副作用的操作。为了能够在测试中 mock 这些操作,通常的做法就是抽象一层 protocol 出来,然后编写不同的实现。 比如需要处理一个简陋的注册业务(示例省略了一点细节),需要用户输入信息后发送网络请求,成功后返回对应用户对象。 首先为网络请求定义一个 protocol: protocol SignUpRepositoryProtocol: RepositoryProtocol { func handleSignUp(name: String, email: String, pwd: String) -> AnyPublisher<User, Error> } 对其进行实现: struct SignUpRepository: SignUpRepositoryProtocol { func handleSignUp(name: String, email: String, pwd: String) -> AnyPublisher<User, Error> { client(.signUp(name, email, pwd)) .map .decode .eraseToAnyPublisher() } } 将其注入到 ViewModel 或是 Interactor 中(取决于你采取的架构是什么 :p ),并且调用对应方法: class SignUpViewModel { enum State { case loading case success case failed } let repository: SignUpRepositoryPortocol var state: State = .loading init(repository: SignUpRepositoryPortocol) { self.repository = repository } func onSubmit(name: String, email: String, pwd: String) { repository.hanldeSignUp(name: name, email: email, pwd: pwd) .sink {[weak self] completion in switch completion { case .failure: self.?state = .failed case .finished: break } } receiveValue: {[weak self] result in self?.state = .success }.store(in: &bag) } } 由此如果需要测试对应的方法,只需要再创建一份 MockSignUpRepository 的实现即可,比如想要测试注册成功或失败场景下的处理: struct MockSignUpRepository: SignUpRepositoryPortocol { let shouldSignUpSuccess: Bool func handleSignUp(name: String, email: String, pwd: String) -> AnyPublisher<User, Error> { if shouldSignUpSuccess { Just(User.mock) .mapError{ _ in SignUpError.someError } .eraseToAnyPublisher() } else { Fail(error: SignUpError.someError) .eraseToAnyPublisher() } } } 在编写测试的时候,传入 SignUpViewModel 的依赖替换成我们想要测试的 Mock 实现: ...

March 15, 2022

Swift 状态管理 —— 如何拆分庞大的 reducer

因为项目需要使用 SwiftUI,想起来之前买过喵神的 《SwiftUI 与 Combine 编程》 。书中介绍了 Redux 这一在 Web 前端领域广泛被验证过的数据管理模式是如何通过 Swift 来实现的,非常推荐 SwiftUI 初见者阅读。 在学习过程中还产生了一个疑问,如果 reducer 越来越大,有什么更 “swift” 的办法能解决这一问题呢?(在 Redux.js 中的原生解决方案是 combineReducers) 拆分 Reducer 首先看看问题在代码中的表现是什么样的,假设我们有这样一个 reducer: func appReducer(appState: inout AppState, action: AppAction) -> Void { switch action { case .emailValid(let isValid): appState.settings.isEmailValid = isValid case .register(let email, let password): appState.settings.loginUser = User(email, password) case .login(let email, let password): appState.settings.loginUser = User(email, password) case .logout: appState.settings.loginUser = nil case .loadPokemon(let result): appState.pokemonList.pokemons = result case .favoratePokemon(let pokemon): appState.favoritePokemons.append(pokemon) case .removeFavoritePokemon(let pokemon): let index = appState.favoritePokemons.indexOf(pokemon) appState.favoritePokemons.remove(at: index) } 应用的 action 主要包含三个模块: 账号登录注册注销 对神奇宝贝数据进行加载 处理对神奇宝贝数据的收藏和取消收藏 从这段代码我们很快就能发现,即使只是非常简单的示例也已经包含了不短的代码了。这里还省略掉了处理状态时可能还需要的异步 action 的处理(数据加载等)。这还仅仅只有两个非常简单的界面状态,当面对真实的 app 所需要处理的数十个页面状态会更恐怖。 将 reducer 拆分成如下三个独立 reducer: func accountReducer(appState: inout AppState, action: AppAction) -> Void { switch action { case .emailValid(let isValid): appState.settings.isEmailValid = isValid case .register(let email, let password): appState.settings.loginUser = User(email, password) case .login(let email, let password): appState.settings.loginUser = User(email, password) case .logout: appState.settings.loginUser = nil default: break } func pokemonListReducer(appState: inout AppState, action: AppAction) -> Void { switch action { case .loadPokemon(let result): appState.pokemonList.pokemons = result default: break } func favoritePokemonReducer(appState: inout AppState, action: AppAction) -> Void { switch action { case .favoratePokemon(let pokemon): appState.favoritePokemons.append(pokemon) case .removeFavoritePokemon(let pokemon): let index = appState.favoritePokemons.indexOf(pokemon) appState.favoritePokemons.remove(at: index) default: break } 因为对 reducer 的数量并不确定,所以这里使用可变参数来构建 combine 方法,对传入的 reducer 进行遍历调用处理 appState。 ...

January 16, 2022

SwiftUI 状态管理 —— Composible Binding

在 SwiftUI 中,需要通过数据来驱动 UI 的变化。数据结构抽象描述的质量也影响着我们对 SwiftUI 界面的维护。 通常数据中可能存在很多状态,如果使用很多的 boolean 值来描述这些状态,那么 App 的可维护性可能会大大降低。 管理独立状态的问题 假设我们有一个 App,用户可以在登录与非登录状态下进行操作。所以我们的界面需要兼容这两种状态,其描述可能是这样的: class AppState: ObservableObject { @Published var user: User? = nil @Published var error: Error? = nil var authenticated: Bool { user != nil } var hasError: Bool { error != nil } } 基于这样的状态描述,如果我们想创建一个仅展示用户名的组件大概会是这样: var body: some View { Group { if state.hasError { Text("Oops, sth went wrong: \(state.error!.localizedDescription)") } if state.authenticated { Text("Hello \(state.user?.name ?? "Unknown")!") } else { Text("Hello, stranger") } } } 粗看没有什么问题,实际上在维护这样的数据结构时就需要格外小心了。比如第一次我们登录失败,为了展示错误信息给 error 设置了值之后。必须在登录成功之后要及时地去清空 error,否则即使 state.authenticated 等于 true,用户依然无法看到正确的信息。 这还仅仅是有两个状态的情况下,像这样独立状态属性会带来很大的维护成本,开发者需要牢记各个属性之间的依赖关系,甚至编写界面的时候,还需要注意代码执行顺序。 引入状态机 把状态抽象成带有 associated values 的 enum 是个更好的选择,比如: class AppState: ObservableObject { enum AccountState { case authenticated(User) case unauthenticated case error(Error) } @Published var accountState: AccountState = .unauthenticated } // 界面中的使用 var body: some View { VStack { switch state.accountState { case .authenticated(let user): Text("Hello \(user.name)!") case .unauthenticated: Text("unregister") case .error(let error): Text("Oops, sth went wrong: \(error.localizedDescription)") } } 这样被状态机驱动的界面看起来要直观多了。并且在每个状态中对数据的操作,也由 enum 赋予了隔离能力。 ...

January 3, 2022

JavaScript 既然是单线程语言,为什么 setTimeout 不会阻塞线程?

先看下面的代码 function printHello() { console.log("Hello"); } function printWorld() { console.log("World"); } printHello(); // 输出 Hello printWorld(); // 输出 World 在 JavaScript 中,存在一个全局调用栈(Global Call Stack)。当我们调用 printHello 时,会将该方法加入到栈中,由于 JavaScript 是单线程执行机制(同一时间只执行一个命令),所以会在执行完成了 printHello 之后再执行 printWorld。 那么现在就引入标题中的问题,JavaScript 既然是单线程语言,为什么 setTimeout 不会阻塞线程? function printHello() { console.log("Hello"); } function printWorld() { console.log("World"); } setTimeout(printHello, 1000); printWorld(); 表面上来看 setTimeout 也是一个方法,他的定义可能是这样: function setTimeout(callbackFunc, interval) { // .... } 那么按照 JS 单线程理论来说,应该是先将 setTimeout 方法压入全局调用栈,并且执行该方法,等待 1 秒钟,然后再执行 printWorld 才对。但实际上我们都知道,打印的结果会是 “World” 然后 “Hello”,这是为什么? Web Browser API & Callback Queue 事实上 setTimeout 并不是完全是 JS 代码,而是属于 Web Browser API 中的方法。就像名字中所指的那样, JS 调用了 setTimeout 之后,浏览器(Web Browser)会去创建一个 timer,同时将我们传入 setTimeout 的方法 - printHello 加入到 Callback Queue(回调队列) 中。 ...

August 19, 2019

React 进阶模式之复合组件(Compound Component)

复合组件是什么 编写页面时,经常存在多个子组件的展示,是依赖于同一个数据源的情况。 比如单选框: <Switcher> <Switch on={this.props.selecting == 'React'}>React</Switch> <Switch on={this.props.selecting == 'Vue'}>Vue</Switch> </Switcher> 我们可以看到,所有的 Switch 的数据都需要对 selecting 的值进行判断,并且代码中其实只有 this.props.selecting == 后面的部分不同,如果能改写成这样: static Switcher.React = ({selecting}) => ( <Switch on={selecting === 'React'}>React</Switch> ) static Switcher.Vue = ({selecting}) => ( <Switch on={selecting === 'Vue'}>Vue</Switch> ) <Switcher selecting={this.props.selecting}> <Switcher.React/> <Switcher.Vue/> </Switcher> 隐式地将父组件的数据传递给子组件,其显示逻辑交由给子组件自行处理,代码的组织结构将会清晰很多。后续即使需求变动,数据的传递改变也并不需要我们操心(不需要一个子组件一个子组件地添加传递),只需要修改 Switcher 子控件内部处理逻辑即可。 那么要怎么实现这个隐式数据传递呢? 可以通过 React.Children.map 和 React.cloneElement 这两个 API 来实现。 React.Children.map 与 React.cloneElement 在 render 中我们可以使用 React.Children.map 来获取到 Switcher 中的子组件,然后通过 React.cloneElement 对组件进行克隆及数据传递: render() { return React.Children.map(this.props.children, child => React.cloneElement(child, { on: this.state.on, toggle: this.toggle, }), ) } 这样,即使我们在使用 Switcher.React 和 Switcher.Vue 时,没有显式地传递参数,子组件也能获取数据。 这里 React.Children.map 与 this.props.children.map 并不等价,后者在只有一个子组件的时候,返回的不是数组,而是唯一的那个组件。 React.Children.map 的局限性 上面代码有个问题是,如果出现了更多层级的子组件,那么参数传递只会到第一层。 <Switcher selecting={this.props.selecting}> <Switcher.React/> <div> <Switcher.Vue/> </div> </Switcher> 这样写会提示传递了错误的参数给 div,因为我们 React.Children.map 只能获取到第一层子组件([Switcher.React, div])。 ...

August 12, 2019

翻译 - 图像优化

翻译自 Optimizing Images by Jordan Morgan 有句话说:最好的照相机就是在你身边的那台。 如果这句俗语是对的,那么毫无疑问地— iPhone 是这个星球上最重要的相机, 并且我们的业界也证明了这一点。 在度假中? 如果没有在你的 Instagram Story 中留下几张照片,那就不算发生过。 爆炸新闻? 立刻打开 Twitter 来查看哪些媒体在通过照片实时报道事件。 等等。 由于图像在各个平台无处不在的出现,在低性能且内存紧张的情况下展示它们,会很容易地造成失控。 如果我们知道 UIKit 底层到底发生了什么,为什么以及如何处理图像,那么我们可以节省大量的资源开销,并且逃脱无情的系统清除制裁。 理论上来说 突击测验 - 这张我女儿的 266 KB 字节大小(并且还蛮时尚的)的照片,在一个 iOS App 中会展示需要用到多少内存? 剧透一下 - 不是 266 KB,也不是 2.66 MB,而是大概 14 MB。 为什么? 本质上来说 iOS 申请内存是根据图像的尺寸 - 而图像的文件大小反而影响不大。 这张图片的尺寸是 1718 x 2048 像素。 假设每个像素会占用 4 个字节: 1718 * 2048 * 4 / 1024 / 1024 = 13.42 MB 大约 ...

June 19, 2019

Platforms State of the Union(WWDC 2019)

SwiftUI 在 View 层级提供了四种特性: Declarative 通过声明式的语句来描述 UI 布局, 样式, 动画等. Automatic 可交互形动画, 动态字号, 夜间模式都可以通过配置来轻松实现. Compositional 组合性. 各种控件都能极其方便地组合在一起, 远比 UIStackView 方便. VStack(alignment: .leading) { Text(item.title) Text(item.subtitle) } Consistent 自带 Reactive 特性. 将 Model 对象继承自 BindableObject, 并且声明属性为 @State 即可获得当属性改变时, UI 控件自动更新的效果. 真的如果如此美好, 超级吃性能的 xib 和 storyboard 是不是可以退出舞台了. Xcode 11 Live Development 直接在 Xcode Preview 中拖动控件即可生成对应的 SwiftUI 代码. 对应的修改 SwiftUI 代码也能实时在 Preview 中响应. Preview 还能通过提供一个 PreviewProvider 来为其提供数据填充展示, 样式更改甚至循环语句来生成多个 Preview 同时查看控件在夜间模式和白日模式下不同的效果. Preview 部署在设备上也能热加载. Package Management Swift 终于有自己的 Package manage 了. 并且和 Xcode 进行了深度整合. ...

June 5, 2019

如何实现 JavaScript 函数参数必填的支持?

JS 在 ES6 的中新增了函数参数指定默认值的支持: const Greeting = (name="Joeytat") => { console.log(`Hello ${name}`) } Greeting() // Hello Joeytat 那我们就可以利用这一特性, 将一个会抛出异常的方法作为默认参数传递. const Greeting = (name=EmptyPropertyException("name")) => { console.log(`Hello ${name}`) } const EmptyPropertyException = (propertyName) => { throw Error(`${propertyName} 为必填参数`) } Greeting() // 抛出异常: "Error: name 为必填参数" 这样如果没有传递参数就会抛出异常, 并且带有友好的提示了.

June 3, 2019

2018

刚毕业那阵儿还每年都写好长的总结, 然后这两年变懒了. 今年又想再记录一下了嘿 | ᐕ)⁾⁾ 年度 App: 多邻国 零基础学语言的感觉很不错. 年度电影: 《三块广告牌》 年度漫画: 《只有我不存在的城市》 忘了在哪被人推荐的了, 被安利到的话大概是这么说的「非常庆幸在没有被剧透的情况下一口气看完了」. 看完了之后感觉果然如此. 而且漫画真是存在着动画无法表现出的节奏感啊. 年度游戏: 《神界原罪 2》 接触的第一款 CRPG, 有趣到什么程度呢? 从游戏体验时发出「wow, 居然还能这样?」的频率来看, 和《塞尔达: 旷野之息》差不多吧. 年度虚构类图书: 《剑来》 不知道为啥现在提起网络文学, 多数时候对方都还是觉得「格」不够. 可现在的网络小说与《明报》连载的武侠小说, 有多大的区别? 年度非虚构类图书: 《邻人之妻》 我姓王和我看这本书没有任何联系(认真脸), 真的是好奇性美国解放运动到底是怎么产生的而看的. 年度电器: Sony 9000E 电视 大屏幕 4k HDR 的全新体验让我想把之前在显示器上玩的好游戏看的好电影都重新来一次. 年度音乐: 《生きていたんだよな(她曾活过啊)》 歌词很棒? 看到年度音乐四个字, 脑子里第一首出现的歌. 年度视频: 井越的 vlog——《别再问我什么是 2017》 看了几十个 Casey Neistat 的 vlog 也没能让自己行动起来用视频记录生活. 但看完这个视频的第二天, 就开始尝试着在大街上对着手机镜头说话啦. 年度电子产品: iPhoneX 用来拍了不少视频, 照片. 人像模式拍出的照片, 会让我这个摄影门外汉产生一种「自己拍的还不错嘛」的错觉, 从而达到了要经常掏出手机记录生活的目标. 希望明年也能多创造一些东西, 能够在 19 年的年终总结中, 选出自己满意的年度 XXX 吧

December 28, 2018

为什么 Cocoapods 1.5 支持编译静态库之后大家这么高兴?

昨天在 Twitter 上看到 TualatriX 说把私有库都通过 Cocoapods 编译成静态库之后很爽, 就有点好奇到底是爽在哪里. 于是去搜了一下, 原来是前段时间(大半年前吧…), Cocoapods 发布了 1.5 的 release note, 宣布支持 Swift 静态库编译. 并且文中提到了对于担心动态二进制文件影响应用启动速度的人来说, 这是个了不起的更新. jh 那又是为什么 App 使用静态库会比动态库有更快的启动速度呢? 又跑去搜了一下官方文档. 打开 Dynamic Library Programming Topics 开头就看到: This article introduces dynamic libraries and shows how using dynamic libraries instead of static libraries reduces both the file size and initial memory footprint of the apps that use them. 这篇文章主要介绍了动态库, 并且展示了通过使用动态库而不是静态库, 是如何缩减了应用的大小和初始内存空间的. ┻━┻ (ヽ(`Д ́)ノ( ┻━┻ 这和说好的不一样啊? ...

December 27, 2018