A base architecture written with Swift and protocol-oriented, for buildind apps easily and quickly
A base architecture written in swift and protocol oriented.
Currently not provide a stable version so please use branch "master" instead.
dependencies: [
.package(url: "https://github.com/Mioke/SwiftyArchitecture.git", branch: "master"),
]
or if you are coding with a iOS application project, please add this repository as a git submodule:
git submodule add -b master https://github.com/Mioke/SwiftyArchitecture.git <path>
git submodule init
git submodule update
After that please click the project file in your Xcode and find "Package Dependency" tab, click +
button and then click Add Local...
, finally please select the path of submodule and the program will automatically find the available package in the folder. Add the package's framework to your project's target and all done.
pod 'SwiftyArchitecture'
# or choose on your need
pod 'SwiftyArchitecture/AppDock'
pod 'SwiftyArchitecture/Componentize'
pod 'SwiftyArchitecture/Networking'
pod 'SwiftyArchitecture/RxExtension'
# Write tests
pod 'SwiftyArchitecture/Testable'
Server
Provide some basic functionalities of a server like url configuation, environments switch etc. In test Mode, offline the server.
let server: Server = .init(live: cdn.config.liveURL,
customEnvironments: [
.custom("Dev"): cdn.config.devURL,
.custom("Staging"): cdn.config.stagingURL,
])
#if DEBUG
server.switch(to: .custom("Dev"))
#endif
You can comstomize the operation of dealing response data now, just subclass from Server
and conform to protocol ServerDataProcessProtocol
like:
func handle(data: Any) throws -> Void {
if let dic = data as? [String: Any],
let errorCode = dic["error_code"] as? Int,
errorCode != 0 {
throw NSError(domain: kYourErrorDomain,
code: errorCode,
userInfo: [NSLocalizedDescriptionKey: message])
}
}
API
Now you can manager request with API<ApiInfoProtocol>
, creating a class conformed to ApiInfoProtocol
, only need to provide some infomation about the API and set where the callback is, you are already finished the configuration of an API.
typealias RequestParam = [String, Any]
var apiVersion: String {
get { return "v2" }
}
var apiName: String {
get { return "user/login" }
}
var server: Server {
get { return mainServer }
}
typealias ResultType = _User
static var responseSerializer: ResponseSerializer<ResultType> {
return JSONResponseSerializer<ResultType>()
}
The API provide some basic method like:
public func sendRequest(with params: [String: Any]?) -> Void
Using chaining syntax to request data:
api.sendRequest(with: nil).response({ (api, user, error) in
if let error = error {
// deal error
}
if let user = user {
// do response if have data
}
})
ApiManager provides an Observable
for you, you can transfrom it or directly bind it to something:
api.rx.sendRequest(with: params)
.flatMap {
...
return yourResultObservable
}
.bind(to: label.rx.text)
.dispose(by: bag)
See this documentation for detail, include App Context
& User Context
& Store
.
SwiftyArchitecture
provides a data center, which can automatically manage your models, data and requests. This function is based on Reactive Functional Programing
, and using RxSwift
. The networking kit is using API
and database is Realm
.
It provides an accessor called Accessor<Object>
, and all data or models can be read throught this DAO.
Example:
Firstly, define your data model in model layer and your database model in Realm
.
class User: RealmSwift.Object {
@Persisted(primaryKey: true)
var userId: String = ""
@Persisted
var name: String = ""
}
Then there may have a API which request data relevant to the User
model:
final class UserAPI: ApiInfoProtocol {
struct Param: Codable {
let uids: [String]
}
typealias RequestParam = UserAPI.Param
struct Reply: Codable {
struct User: Codable {
var userId: String
var name: String
var version: String?
}
var users: [Reply.User]
}
typealias ResultType = Reply
static var apiVersion: String {
get { return "v1" }
}
static var apiName: String {
get { return "getUserInfo" }
}
static var server: Server {
get { return getServer() }
}
static var httpMethod: Alamofire.HTTPMethod {
get { return .get }
}
static var responseSerializer: ResponseSerializer<Reply> {
return JSONCodableResponseSerializer<Reply>()
}
}
To manage this User
model, it must be conformed to the protocol DataCenterManaged
, this protocol defines how data center should do with this model.
extension User: DataCenterManaged {
// define API's type
typealias APIInfo = UserAPI
// defines how transform data from API to data base object.
static func serialize(data: UserAPI.Reply) throws -> [RealmSwift.Object] {
let result: [Object] = data.users.map { item in
let user = User()
user.name = item.name
user.userId = item.userId
return user
}
if data.isFinal {
// do something else
}
return result
}
}
Then you can read the data in database and use it by using Accessor<User>
.
// read all users and display on table view.
Accessor<User>.context(.persist).all
.map { $0.sorted(by: <) }
.map { [AnimatableSectionModel(model: "", items: $0)] }
.bind(to: tableView.rx.items(dataSource: datasource))
.disposed(by: disposeBag)
And then you can update models throught requests, and when data changed, the Observable<User>
will notify observers the new model is coming. And data center will save the newest data to database. For developers of in feature team, they should only need to forcus on models and actions.
print("Show loading")
let request: Request<User> = .init(params: .init(uids: ["10025"]))
DataCenter.update(with: request)
.subscribe({ event in
switch event {
case .completed:
print("Hide loading")
case .error(let error as NSError):
print("Hide loading with error: \(error.localizedDescription)")
}
})
.disposed(by: self.disposeBag)
Support using cocoapods
to modulize your project. You can separate some part of code which is indepence enough, and put the code into a pod
repo. And finally the main project maybe have no code any more, when building your app, the cocoapods
will install all the dependencies together and generate your app.
The good things of modulization
or componentization
are
pod
for development and generate app;cocoapods package
to generate binary library, fasten the app package process.The bad things of it are (for now)
pods
and their git repos, like auto generate version, auto update Podfile
etc.So, this function should depend on the situation of your team and your project. ;)
For small-sized teams, I would like to recommend the monorepo
format for your components. This means you can place all your components in one Git repository but in different folders and create a .podspec
for each of them. Then, you can manage them using Git branches instead of publishing them one by one. At the same time, all the code is separated by Cocoapods, which cannot access each other between pods.
For large-sized teams, if you have a chain of tools and utilities to build pods and publish them, and have an integration tool to put them together into a main project, then you can use the multi-repo
format to manage your pods.
Module
Modules should register into the ModuleManager
when app started.
if let url = Bundle.main.url(forResource: "ModulesRegistery", withExtension: ".plist") {
try! ModuleManager.default.registerModules(withConfigFilePath: url)
}
The register .plist
file contains an array of class names, which implement module's protocol, like:
// in public protocol pod:
public extension ModuleIdentifier {
static let auth: ModuleIdentifier = "com.klein.module.auth"
}
public protocol AuthServiceProtocol {
func authenticate(completion: @escaping (User) -> ()) throws
}
// in private pod:
class AuthModule: ModuleProtocol, AuthServiceProtocol {
static var moduleIdentifier: ModuleIdentifier {
return .auth
}
required init() { }
func moduleDidLoad(with manager: ModuleManager) {}
func authenticate(completion: @escaping (User) -> ()) throws {
// do your work
}
}
// in .plist
array:
Auth.AuthModule,
...
Call other module's method like
guard let authModule = try? self.moduleManager?.bridge.resolve(.auth) as? AuthServiceProtocol
else {
fatalError()
}
authModule.authenticate(completion: { user in
print(user)
})
Navigation
TBA
Initiator
When some modules should do some work at the start process, you will need Initiator
and regitster you opartion in it.
// defined in framework
public protocol ModuleInitiatorProtocol {
static var identifier: String { get }
static var operation: Initiator.Operation { get }
static var priority: Initiator.Priority { get }
static var dependencies: [String] { get }
}
// implement in pod repo
extension AuthModule: ModuleInitiatorProtocol {
static var identifier: String { moduleIdentifier }
static var priority: Initiator.Priority { .high }
static var dependencies: [String] { [ModuleIdentifier.application] }
static var operation: Initiator.Operation {
return {
// do something
}
}
}
Almost done
>w<!
Rx
or ReactiveSwift
. Fully use genericity.All the source code is published under the MIT license. See LICENSE file for details.