Taksi is a framework that simplifies backend-driven solutions in Swift.
Taksi is a package containing a ready-to-use backend-driven solution so you can build agnostic flows. You can easily integrate your backend with one of our server-side projects and start wiring up your solutions in your Swift app.
Going backend-driven brings your app:
- Less time waiting for Apple reviews, since in most cases you'll only need a backend deployment to make copy fixes or even change the navigation and the layout of the app.
- Less stress when performing A/B tests. It all can be controlled by your backend and the app just needs to know the contracts.
- Little to no business rules in the frontend.
That being said, scalability comes with a price. Your app may need core changes to integrate with backend-driven solutions, which is not an easy decision to make.
Taksi's main actors are:
Interface
: a model that contains the data the screen will present.Component
: a blueprint for the different parts of the screen.ViewRepresentable
: the representation ofView
s andUIView
s that components and interfaces may be converted into.
The package also eases the pain of having custom actions triggered by components, with the use of Action
. This model is responsible for telling the app what should be done after a Component
action has been triggered, and also for holding the necessary data.
As for integrating with your backend, TaksiService
handles the communication between the API, using the TaksiAPIClient
Protocol
, and your app when decoding the response payload.
Built-in components with their respective view representables are available to be plugged into your app.
Taksi is available for installation via SPM:
dependencies: [
.package(name: "Taksi", url: "https://github.com/taksi-br/Taksi", .upToNextMajor(from: "1.0.0")),
],
.target(
name: "MyApp",
dependencies: [
.product(name: "Taksi", package: "Taksi")
]
)
Let's create a simple label Component
with a dynamic text value for our module:
import SwiftUI
import Taksi
class ItemComponent: DecodableBaseComponent<ItemComponent.Content, ItemComponentView>, DynamicComponent {
final class Content: DynamicComponentContent, Decodable {
struct DynamicData: DynamicComponentData, Equatable {
var value: String
}
private enum CodingKeys: String, CodingKey {
case action
}
var dynamicData: DynamicData
let action: Action?
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
action = try container.decodeIfPresent(AnyAction.self, forKey: .action)?.action
dynamicData = try DynamicData(from: decoder)
}
func update(using dynamicData: DynamicData) {
self.dynamicData.value = dynamicData.value
}
}
override func view(onAction: @escaping (Action) -> Void) -> ItemComponentView? {
return ItemComponentView(content: content, onAction: onAction)
}
}
struct ItemComponentView: View, ViewRepresentable {
@State var content: ItemComponent.Content
let onAction: (Action) -> Void
var body: some View {
Button(action: {
if let action = content.action {
onAction(action)
}
}) {
Text(content.dynamicData.value)
}
}
}
The code above creates a DecodableComponent
, with a dynamic value
String
that can be updated at anytime. A ViewRepresentable
for the ItemComponent
was created, containing its content and an closure that takes any action as argument so any actor can handle actions as they'd like.
To make Taksi aware of our new Component
, we need to create a new feature that is going to handle the creation of our custom types. Features represent modules of your app that implement FeatureProtocol
.
import Foundation
import Taksi
struct MyFeature: FeatureProtocol {
func action(from decoder: Decoder, withIdentifier identifier: String) -> Action? {
return nil
}
func component(from decoder: Decoder, withName name: String) -> (any Component)? {
return try? ComponentIdentifier(rawValue: name)?.metatype.init(from: decoder)
}
}
import Foundation
import Taksi
enum ComponentIdentifier: String {
case itemComponent = "item_component"
var metatype: any DecodableComponent.Type {
switch self {
case .itemComponent:
return ItemComponent.self
}
}
}
In order to tell Taksi that ItemComponent
can be received from the backend, we must create an identifier enum that holds the metatypes for our custom components. Then MyFeature
can simply decode these metatypes.
It's really simple to initialize Taksi's builder:
let featureBuilder = FeatureBuilder(features: [MyFeature()])
And now we're ready to instantiate our View
with our custom Component
:
import SwiftUI
import Taksi
struct ContentView: View {
@StateObject var model: Model
var body: some View {
NavigationStack {
VStack {
ScrollView {
ForEach(model.components, id: \.identifier) { component in
component.view(onAction: onAction(_:))?.asView()
}
}
Spacer()
}
.padding()
.onAppear {
Task {
await model.fetchInitialComponents()
await model.updateDynamicComponentsData()
}
}
}
}
private func onAction(_ action: Action) {}
}
extension ContentView {
final class Model: ObservableObject {
private let taksiService: TaksiServiceProtocol
@Published var components = [any Component]()
init(taksiService: TaksiServiceProtocol) {
self.taksiService = taksiService
}
func fetchInitialComponents() async {
components = await taksiService.fetchInitialComponents(for: "/api_path")
}
func updateDynamicComponentsData() async {
components = await taksiService.updateDynamicComponentsData(for: components, fetching: "/api_path")
objectWillChange.send()
}
}
}
The code above simply creates a View
agnostic from its components. By telling TaksiService
to fetch initial components, the View
can render its skeleton without busy waiting, and then it can fetch and update all the dynamic data under the hood.
Distributed under the GNU General Public License v3.0. See LICENSE.txt
for more information.