SwiftGUI
SwiftGUI is a Swift framework for writing cross-platform GUI applications.
Demo
Currently Linux and MacOS are supported. Windows support is planned.
To run the demo follow the installation instructions for SDL2 below, clone the repository and in the root directory execute swift run TaskOrganizerDemo
.
The code for the demo app can be found in Sources/TaskOrganizerDemo
I'm working on a tutorial.
Table of Contents
- Installation
- Simple Code Example
- Feature Overview
- Current Limitations
- Roadmap
- Contribute
- VSCode Setup on Linux
- Dependencies
Installation
SDL2
The framework depends on SDL2 to create windows and receive events.
On Ubuntu install it with:
sudo apt-get install libsdl2-dev
on MacOS (via homebrew):
brew install sdl2
for other platforms see: Installing SDL.
SwiftGUI
This project is under heavy development. I will not create releases until there is some API stability.
Just use the master branch:
dependencies: [
...,
.package(name: "SwiftGUI", url: "https://github.com/UnGast/swift-gui", .branch("master")),
],
targets: [
...,
.target(name: "SomeTarget", dependencies: ["SwiftGUI", ...])
]
A Swift 5.3 toolchain is required.
Simple Code Example
Result:
import SwiftGUI
public class MainView: ContentfulWidget {
@State
private var counter = 0
@DirectContentBuilder override public var content: DirectContent {
Container().with(classes: ["container"]).withContent { [unowned self] in
Button().onClick {
counter += 1
}.withContent {
Text(ImmutableBinding($counter.immutable, get: { "counter: \($0)" }))
}
}
}
// you can define themes, so this can also be done in three lines
override public var style: Style {
let primaryColor = Color(77, 255, 154, 255)
return Style("&") {
(\.$background, Color(10, 20, 30, 255))
} nested: {
Style(".container", Container.self) {
(\.$alignContent, .center)
(\.$justifyContent, .center)
}
Style("Button") {
(\.$padding, Insets(all: 16))
(\.$background, primaryColor)
(\.$foreground, .black)
(\.$fontWeight, .bold)
} nested: {
Style("&:hover") {
(\.$background, primaryColor.darkened(20))
}
Style("&:active") {
(\.$background, primaryColor.darkened(40))
}
}
}
}
}
When you press the button, the counter should be incremented.
Some additional setup code is necessary to display the window. You can find all of it in Sources/MinimalDemo
Feature Overview
Declarative GUI Structure
Using Swift's function/result builders.
Container().withContent {
Button().withContent {
Text("Hello World")
$0.iconSlot {
Icon(identifier: .party)
}
}
}
List(items).withContent {
$0.itemSlot { itemData in
Text(itemData)
}
}
Custom Widgets
by composing other Widgets
Create reusable views consiting of multiple Widgets. Pass child Widgets to your custom Widget instances by using slots. Parts of the composition API might be renamed in the future.
class MyCustomView: ContentfulWidget, SlotAcceptingWidgetProtocol {
static let childSlot = Slot(key: "child", data: String.self)
let childSlotManager = SlotContentManager(MyCustomView.childSlot)
@DirectContentBuilder override var content: DirectContent {
Container().withContent {
Text("some text 1")
childSlotManager("the data passed to the child slot definition")
Button().withContent {
Text("this Text Widget goes to the default slot of the Button")
}
}
}
}
// use your custom Widget
Container() {
Text("any other place in your code")
MyCustomView().withContent {
$0.childSlot { data in
// this Text Widget will receive the String
// passed to the childSlotManager() call above
Text(data)
}
}
}
by drawing graphics primitives (LeafWidget)
LeafWidgets are directly drawn to the screen. They do not have children.
class MyCustomLeafWidget: LeafWidget {
override func draw(_ drawingContext: DrawingContext) {
drawingContext.drawRect(rect: ..., paint: Paint(color: ..., strokeWidth: ...))
drawingContext.drawLine(...)
drawingContext.drawText(...)
}
}
Styling API similar to CSS
Container().with(classes: ["container"]) {
Button().withContent {
Text("Hello World")
}
}
// select by class
Style(".container") {
(\.$background, .white)
// foreground is similar to color in css, color of text = foreground
(\.$foreground, Color(120, 40, 0, 255))
} nested: {
// select by Widget type
Style("Text") {
// inherit is the default for foreground, so this is not necessary
(\.$foreground, .inherit)
(\.$fontWeight, .bold)
}
// & references the parent style, in this case .container and extends it
// the currently supported pseudo classes are :hover and :active
Style("&:hover") {
(\.$background, .black)
}
}
custom Widgets can have special style properties
class MyCustomWidget {
...
@StyleProperty
public var myCustomStyleProperty: Double = 0.0
...
}
// somewhere else in your code
Style(".class-applied-to-my-custom-widget") {
(\.$myCustomStyleProperty, 1.0)
}
Reactive Widget Content
Update the content and structure of your Widgets when data changes.
class MyCustomWidget: ContentfulWidget {
@State private var someState: Int = 0
@ImmutableBinding private var someStateFromTheOutside: String
public init(_ outsideStateBinding: ImmutableBinding<String>) {
self._someStateFromTheOutside = outSideStateBinding
}
@DirectContentBuilder override var content: DirectContent {
Container().withContent { [unowned self] in
// use Dynamic for changing the structure of a Widget
Dynamic($someState) {
if someState == 0 {
Button().onClick {
someState += 1
}.withContent {
Text("change someState")
}
} else {
Text("someState is not 0")
}
}
// pass a Binding to a child to have it always reflect the latest state
Text($someStateFromTheOutside.immutable)
// you can construct proxy bindings
// in this case the proxy converts the Int property to a String
Text(ImmutableBinding($someState.immutable, get: { String($0) }))
}
}
}
Inject Dependencies Into Widgets
This should be changed so that providing dependencies can be done by using a property wrapper as well. Dependencies are resolved by comparing keys (if given) and types.
class MyCustomWidget: ContentfulWidget {
...
@Inject(key: <nil or a String>) private var myDependency: String
}
class MyCustomParentWidget: ContentfulWidget {
// API will be changed, so that this dependency can be provided by doing:
// @Provide(key: <nil or a String>)
let providedDependency: String = "dependency"
@DirectContentBuilder override var content: DirectContent {
Container().withContent {
MyCustomWidget()
}.provide(dependencies: providedDependency)
}
}
Global App State Management
The approach is similar to Vuex. Defining mutations and actions as enum cases instead of methods allows for automatic recording where and when which change was made to the state.
class MyAppStore: Store<MyAppState, MyAppMutation, MyAppAction> {
init() {
super.init(initialState: MyAppState(
stateProperty1: "initial"))
}
override func perform(mutation: Mutation, state: SetterProxy) {
switch mutation {
case let .setStateProperty1(value):
state.stateProperty1 = value
}
}
override func perform(action: Action) {
switch action {
case .doSomeAsynchronousOperation:
// ... do stuff
// when finished:
commit(.setStateProperty1(resultOfOperation))
}
}
}
struct MyAppState {
var stateProperty1: String
}
enum MyAppMutation {
case .setStateProperty1(String)
}
enum MyAppAction {
case .doSomeAsynchronousOperation
}
Now you can use the store in your whole app like so:
class TheRootView: ContentfulWidget {
let store = MyAppStore()
@DirectContentBuilder override var content: DirectContent {
Container().provide(dependencies: store).withContent {
...
// can be deeply nested
MyCustomWidget()
...
}
}
}
class MyCustomWidget: ContentfulWidget {
@Inject var store: MyAppStore
@DirectContentBuilder override var content: DirectContent {
Container().withContent { [unowned self] in
// the store exposes reactive bindings
// to every state property via store.$state
Text(store.$state.stateProperty1.immutable)
Dynamic(store.$state.stateProperty1) {
// ... everything inside here will be rebuilt
// when stateProperty1 changes
}
Button().onClick {
store.commit(.setStateProperty1("changed by button click"))
}.withContent {
Text("change stateProperty1")
}
}
}
}
Current Limitations
- only runs on Linux (tested on Ubuntu 20.04) and MacOS (tested on MacOS 10.15)
- depends on SDL2 for handling cross platform window management
- depends on NanoVG (specifically on the OpenGL 3.3 implementation of NanoVG) for rendering primitives (line, rect, ...)
- a few core Widget types (Container, Button, Text, TextInput, ...) are available
- the graphics api has only been implemented in so far as to be able to create the above demos
- everything is redrawn on every frame
- animations, transitions are not yet supported
- only one layout type is well supported, very similar to CSS flexbox, but does not yet support line breaks
Roadmap
- Windows support
- WebAssembly support
- extend drawing api
- gradients
- rounded rects
- arbitrary paths
- ...
- consider adding Skia as additional drawing backend
- more core Widgets
- RadioButton
- Checkbox
- Image
- Textarea
- ...
- full flexbox layout system
- other layout systems
- absolute
- anchor
- ...
- transitions, animations
- optimize drawing, only redraw on update
Contribute
The main ways to contribute currently are feature requests, opinions on API design and reporting bugs. There are no guidelines. Just open an issue.
VSCode Setup on Linux
Copied from: github.com/ewconnell/swiftrt
Install the following extensions:
Swift Language (Martin Kase)
CodeLLDB (Vadim Chugunov)
It is very important that settings.json contains the following entry to pickup the correct lldb version from the toolchain. Substituting PathToSwiftToolchain with wherever you installed the toolchain. { "lldb.library": "PathToSwiftToolchain/usr/lib/liblldb.so" }
SourceKit-LSP (Pavel Vasek)
There is a version of the server as part of the toolchain already, so you don't need to build it. Make sure to configure the extension "sourcekit-lsp.serverPath": "PathToSwiftToolchain/usr/bin/sourcekit-lsp".
Dependencies
This package depends on:
GL (OpenGL loader written in Swift): github.com/kelvin13/swift-opengl
CombineX (open source implementation of Apple's Combine framework)
Swim (Image handling): github.com/t-ae/swim.git
Cnanovg (NanoVG wrapper for Swift): github.com/UnGast/Cnanovg.git