Fisticuffs is a data binding framework for Swift, inspired by Knockout and shares many of the same concepts:
-
Declarative Bindings
Simplify your view controllers by setting up all your view logic in one place. No need to implement target-actions, delegate methods, etc.
-
Automatic Updates
When your data changes, your UI is automatically updated to reflect those changes. Likewise, when your users interact with your UI, your data is automatically updated.
-
Automatic Dependency Tracking
Easily and implicitly setup a graph of dependencies. When an underlying dependency changes its value, Fisticuffs will ensure those changes are propagated up.
Since a code snippet can be worth a thousand words...
class LoginViewController: UIViewController {
@IBOutlet var usernameField: UITextField!
@IBOutlet var passwordField: UITextField!
@IBOutlet var loginButton: UIButton!
let username = CurrentValueSubscribable("")
let password = CurrentValueSubscribable("")
lazy var inputIsValid: Computed<Bool> = Computed { [weak self] in
let user = self?.username.value
let pass = self?.password.value
return user?.isEmpty == false && pass?.isEmpty == false
}
override viewDidLoad() {
super.viewDidLoad()
// bind the text in our username & password fields to their
usernameField.b_text.bind(username)
passwordField.b_text.bind(password)
// only enable the login button if they've entered an username and password
loginButton.b_enabled.bind(inputIsValid)
// do the login when the user taps the login button
loginButton.b_onTap.subscribe { [weak self] in
LoginManager.doLogin(self?.username.value, self?.password.value)
}
}
}
The CurrentValueSubscribable
class is one of the basic building blocks of Fisticuffs. It stores a value and notifies any subscribers when it changes:
let currentValueSubscribable = CurrentValueSubscribable<String>("")
currentValueSubscribable.subscribe { oldValue, newValue in
print(newValue)
}
currentValueSubscribable.value = "Hello, world"
// prints "Hello, world"
Computed
is the read-only sibling of CurrentValueSubscribable
. It uses a closure to compute its value. Additionally, it automatically updates its value when any of its CurrentValueSubscribable
(and Computed
) dependencies change. For example:
let name = CurrentValueSubscribable<String>("")
let greeting: Computed<String> = Computed {
return "Hello, " + name.value
}
greeting.subscribe { oldValue, newValue in
print(newValue)
}
name.value = "world"
// prints "Hello, world" because the change to `name` is propagated up to `greeting`
Finally, the Event
class provides support for broadcasting events to its subscribers. It shares a similar interface to CurrentValueSubscribable
& Computed
:
let event = Event<String>()
event.subscribe { _, str in
print(str)
}
event.fire("Hello, world")
// prints "Hello, world"
As a side note, CurrentValueSubscribable
, Computed
, and Event
all inherit from Subscribable
to provide a common interface to subscribing to changes/events.
BindingHandler
s describe how a raw data value (ie Int
, String
, NSDate
, etc..) should be applied to a property
(UILabel.text
, UIImageView.image
, etc..)
Some useful examples of what can be done with BindingHandlers:
BindingHandlers.loadImage()
- enables binding aNSURL
to aUIImage
propertyBindingHandlers.formatSalary()
- formats a raw salaryInt
as a nice string (ie100k
)BindingHandlers.autoupdatingTimeAgo()
- enables binding aNSDate
to aString
property (ie.UILabel.text
) that shows values like "5 minutes ago", etc.. (and automatically updates the displayed string as time passes)
Generally the raw value (ie a Double
for fantasy points) should be bubbled up to the UI in the view model, so that it
can be formatted as need for that view.
Many of the UIKit
classes have been extended to allow binding their properties to Subscribables
. These properties are generally to
UIKit | Fisticuffs |
---|---|
UILabel .text |
UILabel .b_text |
UITextField .text |
UITextField .b_text |
UIButton .enabled |
UIButton .b_enabled |
UISwitch .on |
UISwitch .b_on |
etc... |
To bind a Subscribable
(ie: CurrentValueSubscribable
, Computed
, etc...) to these, the bind()
method can be used.
let messageLabel: UILabel = ...
let message = CurrentValueSubscribable("")
messageLabel.b_text.bind(message)
UI events are exposed as Event
's, making it easy to attach behaviour to controls. For example:
let button: UIButton = ...
button.b_onTap.subscribe {
print("Pressed button!")
}
Fisticuffs provides support for easily binding data to UITableViews / UICollectionViews. See example below:
let tableView: UITableView = ...
let days = CurrentValueSubscribable(["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"])
tableView.registerClass(UITableViewCell.self forCellReuseIdentifier: "Cell")
tableView.b_configure(days) { config in
config.useCell(reuseIdentifier: "Cell") { day, cell in
cell.textLabel?.text = day
}
config.allowsMoving = true // let user reorder the days
config.allowsDeletion = true // let user delete days (good-bye Monday! :)
}
Some interesting things to note:
- Since we set
allowsMoving
&allowsDeletion
, users can move & delete rows. The underlying data (days
) will be updated automatically - Any updates we make to
days
in code will be propagated up to the table view (including animating insertions, removals, etc..)
Similar bindings exist for UICollectionView
Requirements:
- Xcode 11 / Swift 5.0
- iOS 8+
-
If you haven't already, install CocoaPods and setup your project for use with CocoaPods
-
Add
Fisticuffs
to yourPodfile
:
pod 'Fisticuffs', '0.0.8'
- Run
pod install
NOTE: There may be breaking changes before 1.0, so it is suggested to pin it to a specific version
To add Fisticuffs
to your app using Swift Package Manager, use Apple's guide with url https://github.com/scoremedia/Fisticuffs.git
.
To add Fisticuffs
dependency to your swift package, add the following to your Package.swift
file.
dependencies: [
.package(url: "https://github.com/scoremedia/Fisticuffs.git", .upToNextMajor(from: "0.0.13"))
]
-
If you haven't already, install Carthage & setup your project for use with it. See here.
-
Add
Fisticuffs
to yourCartfile
:
github "scoremedia/Fisticuffs" == 0.0.8
- Run
carthage update
May be breaking changes before 1.0, so it is suggested to pin it to a specific version
-
Download this repository (if using Git, you can add it as a submodule)
-
Drag
Fisticuffs.xcodeproj
into your Xcode project or workspace -
Add
Fisticuffs.framework
to your app's Embedded Binaries and Linked Frameworks and Libraries
Tests and some examples are provided in the Fisticuffs.xcworkspace
git submodule update --init --recursive
-
Open
Fisticuffs.xcworkspace
, select theFisticuffs
scheme -
Product → Test
Fisticuffs is released under the MIT license.