Learning from Open Source
Khoa Pham
Posted on September 21, 2018
Here is what I learned from some of the open source proiects
Pure: Generic factory
From devxoul/Pure:Sources/Pure/FactoryModule.swift@master
public protocol FactoryModule: Module {
/// A factory for `Self`.
associatedtype Factory = Pure.Factory<Self>
/// Creates an instance of a module with a dependency and a payload.
init(dependency: Dependency, payload: Payload)
}
From devxoul/Pure:Sources/Pure/Factory.swift@master
open class Factory<Module: FactoryModule> {
private let dependencyClosure: () -> Module.Dependency
/// A static dependency of a module.
open var dependency: Module.Dependency {
return self.dependencyClosure()
}
/// Creates an instance of `Factory`.
///
/// - parameter dependency: A static dependency which should be resolved in a composition root.
public init(dependency: @autoclosure @escaping () -> Module.Dependency) {
self.dependencyClosure = dependency
}
/// Creates an instance of a module with a runtime parameter.
///
/// - parameter payload: A runtime parameter which is required to construct a module.
open func create(payload: Module.Payload) -> Module {
return Module.init(dependency: self.dependency, payload: payload)
}
}
From devxoul/Pure:Tests/PureTests/PureSpec.swift@master#L72
let factory = FactoryFixture<Dependency, Payload>.Factory(dependency: .init(
networking: "Networking A"
))
let instance = factory.create(payload: .init(id: 100))
expect(instance.dependency.networking) == "Networking A"
expect(instance.payload.id) == 100
retrofit: Making deferred
override fun adapt(call: Call<T>): Deferred<T> {
val deferred = CompletableDeferred<T>()
deferred.invokeOnCompletion {
if (deferred.isCancelled) {
call.cancel()
}
}
call.enqueue(object : Callback<T> {
override fun onFailure(call: Call<T>, t: Throwable) {
deferred.completeExceptionally(t)
}
override fun onResponse(call: Call<T>, response: Response<T>) {
if (response.isSuccessful) {
deferred.complete(response.body()!!)
} else {
deferred.completeExceptionally(HttpException(response))
}
}
})
return deferred
}
On: Generic extension with associatedtype protocol
I like extensions, and I like to group them under 1 common property to easily access. This also makes it clear that these all belong to the same feature and not to confuse with Apple properties.
Grouping all related extensions
This is how I do it in Anchor and On
activate(
a.anchor.top.left,
b.anchor.top.right,
c.anchor.bottom.left,
d.anchor.bottom.right
)
textField.on.text { text in
print("textField text has changed")
}
textField.on.didEndEditing { text in
print("texField has ended editing")
}
Generic extension
For On, it is a bit tricky as it needs to adapt to different NSObject subclasses. And to make auto completion work, meaning that each type of subclass gets its own function hint, we need to use generic and associatedtype protocol.
You can take a look at Container and OnAware
public class Container<Host: AnyObject>: NSObject {
unowned let host: Host
init(host: Host) {
self.host = host
}
}
public protocol OnAware: class {
associatedtype OnAwareHostType: AnyObject
var on: Container<OnAwareHostType> { get }
}
RxCocoa
RxSwift has its RxCocoa that does this trick too, so that you can just declare
button.rx.tap
textField.rx.text
alertAction.rx.isEnabled
The power lies in the struct Reactive and ReactiveCompatible protocol
public struct Reactive<Base> {
/// Base object to extend.
public let base: Base
/// Creates extensions with base object.
///
/// - parameter base: Base object.
public init(_ base: Base) {
self.base = base
}
}
public protocol ReactiveCompatible {
/// Extended type
associatedtype CompatibleType
/// Reactive extensions.
static var rx: Reactive<CompatibleType>.Type { get set }
/// Reactive extensions.
var rx: Reactive<CompatibleType> { get set }
}
extension ReactiveCompatible {
/// Reactive extensions.
public static var rx: Reactive<Self>.Type {
get {
return Reactive<Self>.self
}
set {
// this enables using Reactive to "mutate" base type
}
}
/// Reactive extensions.
public var rx: Reactive<Self> {
get {
return Reactive(self)
}
set {
// this enables using Reactive to "mutate" base object
}
}
}
Here UIButton+Rx you can see how it can be applied to UIButton
extension Reactive where Base: UIButton {
/// Reactive wrapper for `TouchUpInside` control event.
public var tap: ControlEvent<Void> {
return controlEvent(.touchUpInside)
}
}
BackchannelSDK-iOS: Using Coordinator
The Coordinator pattern can be useful to manage dependencies and handle navigation for your view controllers. It can be seen from BackchannelSDK-iOS, take a look at BAKCreateProfileCoordinator for example
@implementation BAKCreateProfileCoordinator
- (instancetype)initWithUser:(BAKUser *)user navigationController:(UINavigationController *)navigationController configuration:(BAKRemoteConfiguration *)configuration {
self = [super init];
if (!self) return nil;
_navigationController = navigationController;
_user = user;
_profileViewController = [[BAKProfileFormViewController alloc] init];
[self configureProfileForm];
_configuration = configuration;
return self;
}
- (void)start {
[self.profileViewController updateDisplayName:self.user.displayName];
[self.navigationController pushViewController:self.profileViewController animated:YES];
}
- (void)profileViewControllerDidTapAvatarButton:(BAKProfileFormViewController *)profileViewController {
BAKChooseImageCoordinator *imageChooser = [[BAKChooseImageCoordinator alloc] initWithViewController:self.navigationController];
imageChooser.delegate = self;
[self.childCoordinators addObject:imageChooser];
[imageChooser start];
}
- (void)imageChooserDidCancel:(BAKChooseImageCoordinator *)imageChooser {
[self.childCoordinators removeObject:imageChooser];
}
Look how it holds navigationController as root element to do navigation, and how it manages childCoordinators
Kickstarter: Manage dependencies
Another cool thing about ios-oss is how it manages dependencies. Usually you have a lot of dependencies, and it’s good to keep them in one place, and inject it to the objects that need.
The Environment is simply a struct that holds all dependencies throughout the app
/**
A collection of **all** global variables and singletons that the app wants access to.
*/
public struct Environment {
/// A type that exposes endpoints for fetching Kickstarter data.
public let apiService: ServiceType
/// The amount of time to delay API requests by. Used primarily for testing. Default value is `0.0`.
public let apiDelayInterval: DispatchTimeInterval
/// A type that exposes how to extract a still image from an AVAsset.
public let assetImageGeneratorType: AssetImageGeneratorType.Type
/// A type that stores a cached dictionary.
public let cache: KSCache
/// ...
}
Then there’s global object called AppEnvironment that manages all these Environment in a stack
public struct AppEnvironment {
/**
A global stack of environments.
*/
fileprivate static var stack: [Environment] = [Environment()]
/**
Invoke when an access token has been acquired and you want to log the user in. Replaces the current
environment with a new one that has the authenticated api service and current user model.
- parameter envelope: An access token envelope with the api access token and user.
*/
public static func login(_ envelope: AccessTokenEnvelope) {
replaceCurrentEnvironment(
apiService: current.apiService.login(OauthToken(token: envelope.accessToken)),
currentUser: envelope.user,
koala: current.koala |> Koala.lens.loggedInUser .~ envelope.user
)
}
/**
Invoke when we have acquired a fresh current user and you want to replace the current environment's
current user with the fresh one.
- parameter user: A user model.
*/
public static func updateCurrentUser(_ user: User) {
replaceCurrentEnvironment(
currentUser: user,
koala: current.koala |> Koala.lens.loggedInUser .~ user
)
}
public static func updateConfig(_ config: Config) {
replaceCurrentEnvironment(
config: config,
koala: AppEnvironment.current.koala |> Koala.lens.config .~ config
)
}
// Invoke when you want to end the user's session.
public static func logout() {
let storage = AppEnvironment.current.cookieStorage
storage.cookies?.forEach(storage.deleteCookie)
replaceCurrentEnvironment(
apiService: AppEnvironment.current.apiService.logout(),
cache: type(of: AppEnvironment.current.cache).init(),
currentUser: nil,
koala: current.koala |> Koala.lens.loggedInUser .~ nil
)
}
// The most recent environment on the stack.
public static var current: Environment! {
return stack.last
}
}
Then whenever there’s event that triggers dependencies update, we call it like
self.viewModel.outputs.logIntoEnvironment
.observeValues { [weak self] accessTokenEnv in
AppEnvironment.login(accessTokenEnv)
self?.viewModel.inputs.environmentLoggedIn()
}
The cool thing about Environment is that we can store and retrieve them
// Returns the last saved environment from user defaults.
public static func fromStorage(ubiquitousStore: KeyValueStoreType,
userDefaults: KeyValueStoreType) -> Environment {
// retrieval
}
And we can mock in tests
AppEnvironment.replaceCurrentEnvironment(
apiService: MockService(
fetchDiscoveryResponse: .template |> DiscoveryEnvelope.lens.projects .~ [
.todayByScottThrift,
.cosmicSurgery,
.anomalisa
]
)
)
Kickstarter: Using Playground
One thing I like about kickstarter-ios is how they use Playground to quickly protoyping views.
We use Swift Playgrounds for iterative development and styling. Most major screens in the app get a corresponding playground where we can see a wide variety of devices, languages and data in real time.
This way we don’t need Injection or using React Native anymore. Take a look at all the pages kickstarter/ios-oss:Kickstarter-iOS.playground/Pages@master
Touchbar simulator: Making macOS app in code
I’m familiar with the whole app structure that Xcode gives me when I’m creating new macOS project, together with Storyboard. The other day I was reading touch-bar-simulator and see how it declares app using only code. See this main.swift
let app = NSApplication.shared
let delegate = AppDelegate()
app.delegate = delegate
app.run()
final class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate {
let controller = IDETouchBarSimulatorHostWindowController.simulatorHostWindowController()!
lazy var window: NSWindow = self.controller.window!
}
Kickstarter: Input and output container
This is a very nifty trick from ios-oss which was built around MVVM pattern. It uses protocol to define input and output, and a container protocol to contain them. Take kickstarter/ios-oss:Library/ViewModels/LoginViewModel.swift@1f5643f for example
public protocol LoginViewModelInputs {
}
public protocol LoginViewModelOutputs {
}
public protocol LoginViewModelType {
var inputs: LoginViewModelInputs { get }
var outputs: LoginViewModelOutputs { get }
}
public final class LoginViewModel: LoginViewModelType, LoginViewModelInputs, LoginViewModelOutputs {
public var inputs: LoginViewModelInputs { return self }
public var outputs: LoginViewModelOutputs { return self }
}
Look how LoginViewModel conforms to 3 protocols. And when you access its input or outputproperties, you are constrained to only LoginViewModelInputs and LoginViewModelOutputs
Suas: Observing object deinit in Swift
-
Today I was browsing through Suas-iOS and the subscription links to life cycle of another object
subscription.linkLifeCycleTo(object: self)
It observes the deinit of another job, interesting approach 👍 , take a look in zendesk/Suas-iOS:Sources/StoreDeinitCallback.swift@master
var deinitCallbackKey = "DEINITCALLBACK_SUAS"
// MARK: Registartion
extension Suas {
static func onObjectDeinit(forObject object: NSObject,
callbackId: String,
callback: @escaping () -> ()) {
let rem = deinitCallback(forObject: object)
rem.callbacks.append(callback)
}
static fileprivate func deinitCallback(forObject object: NSObject) -> DeinitCallback {
if let deinitCallback = objc_getAssociatedObject(object, &deinitCallbackKey) as? DeinitCallback {
return deinitCallback
} else {
let rem = DeinitCallback()
objc_setAssociatedObject(object, &deinitCallbackKey, rem, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
return rem
}
}
}
@objc fileprivate class DeinitCallback: NSObject {
var callbacks: [() -> ()] = []
deinit {
callbacks.forEach({ $0() })
}
}
Touchbar simulator: Take an app from a private framework
The other day I was browsing through sindresorhus/touch-bar-simulator, it was very brilliant of him to pull IDETouchBarSimulatorHostWindowController from DFRSupportKit.framework. This is worth checking out
Posted on September 21, 2018
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.