Skip to content

Commit 916cad0

Browse files
committed
More progress, but something's still broken
1 parent fcfe315 commit 916cad0

10 files changed

+548
-230
lines changed

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,11 @@ SwiftCrossUI has a variety of backends tailored to different operating systems.
3030

3131
If you use `DefaultBackend`, like the examples do, you can override the default when compiling your app by setting the `SCUI_DEFAULT_BACKEND` environment variable to the name of your desired backend. This can be quite useful when you e.g. want to test the Gtk version of your app while using a Mac.
3232

33-
- `DefaultBackend`: Adapts to your target operating system. On macOS it uses `AppKitBackend`, on Windows it uses `WinUIBackend`, and on Linux it uses `GtkBackend`.
33+
- `DefaultBackend`: Adapts to your target operating system. On macOS it uses `AppKitBackend`, on Windows it uses `WinUIBackend`, on Linux it uses `GtkBackend`, and on iOS and tvOS it uses `UIKitBackend`.
3434
- `GtkBackend`: Works on Linux, macOS, and Windows. Requires gtk 4 to be installed. Supports all SwiftCrossUI features.
3535
- `AppKitBackend`: The native macOS backend. Supports all SwiftCrossUI features.
3636
- `WinUIBackend`: The native Windows backend. Supports most SwiftCrossUI features.
37+
- `UIKitBackend`: The native iOS & tvOS backend. Supports most SwiftCrossUI features.
3738
- `QtBackend`: ***Experimental***, requires `qt5` to be installed, and currently supports a very limited subset of SwiftCrossUI features.
3839
- `CursesBackend`: ***Experimental***, requires `curses` to be installed, and supports a *very very* limited subset of SwiftCrossUI features.
3940

Sources/UIKitBackend/BaseWidget.swift

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
//
2+
// Untitled.swift
3+
// swift-cross-ui
4+
//
5+
// Created by William Baker on 1/10/25.
6+
//
7+
8+
import UIKit
9+
10+
public class BaseWidget: UIView {
11+
private var leftConstraint: NSLayoutConstraint?
12+
private var topConstraint: NSLayoutConstraint?
13+
private var widthConstraint: NSLayoutConstraint?
14+
private var heightConstraint: NSLayoutConstraint?
15+
16+
internal var x = 0 {
17+
didSet {
18+
if x != oldValue {
19+
updateLeftConstraint()
20+
}
21+
}
22+
}
23+
24+
internal var y = 0 {
25+
didSet {
26+
if y != oldValue {
27+
updateTopConstraint()
28+
}
29+
}
30+
}
31+
32+
internal var width = 0 {
33+
didSet {
34+
if width != oldValue {
35+
updateWidthConstraint()
36+
}
37+
}
38+
}
39+
40+
internal var height = 0 {
41+
didSet {
42+
if height != oldValue {
43+
updateHeightConstraint()
44+
}
45+
}
46+
}
47+
48+
init() {
49+
super.init(frame: .zero)
50+
51+
self.clipsToBounds = true
52+
self.translatesAutoresizingMaskIntoConstraints = false
53+
}
54+
55+
@available(*, unavailable)
56+
public required init?(coder: NSCoder) {
57+
fatalError("init(coder:) is not used for this view")
58+
}
59+
60+
private func updateLeftConstraint() {
61+
leftConstraint?.isActive = false
62+
guard let superview else { return }
63+
leftConstraint = self.leftAnchor.constraint(
64+
equalTo: superview.leftAnchor, constant: CGFloat(x))
65+
leftConstraint!.isActive = true
66+
}
67+
68+
private func updateTopConstraint() {
69+
topConstraint?.isActive = false
70+
guard let superview else { return }
71+
topConstraint = self.topAnchor.constraint(
72+
equalTo: superview.topAnchor, constant: CGFloat(y))
73+
topConstraint!.isActive = true
74+
}
75+
76+
private func updateWidthConstraint() {
77+
widthConstraint?.isActive = false
78+
widthConstraint = self.widthAnchor.constraint(equalToConstant: CGFloat(width))
79+
widthConstraint!.isActive = true
80+
}
81+
82+
private func updateHeightConstraint() {
83+
heightConstraint?.isActive = false
84+
heightConstraint = self.heightAnchor.constraint(equalToConstant: CGFloat(height))
85+
heightConstraint!.isActive = true
86+
}
87+
88+
public override func didMoveToSuperview() {
89+
super.didMoveToSuperview()
90+
91+
updateLeftConstraint()
92+
updateTopConstraint()
93+
}
94+
}
95+
96+
internal class WrapperWidget<View: UIView>: BaseWidget {
97+
init(child: View) {
98+
super.init()
99+
100+
self.addSubview(child)
101+
child.translatesAutoresizingMaskIntoConstraints = false
102+
NSLayoutConstraint.activate([
103+
child.topAnchor.constraint(equalTo: self.topAnchor),
104+
child.leadingAnchor.constraint(equalTo: self.leadingAnchor),
105+
child.bottomAnchor.constraint(equalTo: self.bottomAnchor),
106+
child.trailingAnchor.constraint(equalTo: self.trailingAnchor),
107+
])
108+
}
109+
110+
override convenience init() {
111+
self.init(child: View(frame: .zero))
112+
}
113+
114+
var child: View {
115+
subviews[0] as! View
116+
}
117+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
//
2+
// Font+UIFont.swift
3+
// swift-cross-ui
4+
//
5+
// Created by William Baker on 1/10/25.
6+
//
7+
8+
import SwiftCrossUI
9+
import UIKit
10+
11+
extension Font {
12+
internal var uiFont: UIFont {
13+
switch self {
14+
case .system(let size, let weight, let design):
15+
let weight: UIFont.Weight =
16+
switch weight {
17+
case .black:
18+
.black
19+
case .bold:
20+
.bold
21+
case .heavy:
22+
.heavy
23+
case .light:
24+
.light
25+
case .medium:
26+
.medium
27+
case .regular, nil:
28+
.regular
29+
case .semibold:
30+
.semibold
31+
case .thin:
32+
.thin
33+
case .ultraLight:
34+
.ultraLight
35+
}
36+
37+
switch design {
38+
case .monospaced:
39+
return .monospacedSystemFont(ofSize: CGFloat(size), weight: weight)
40+
default:
41+
return .systemFont(ofSize: CGFloat(size), weight: weight)
42+
}
43+
}
44+
}
45+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
//
2+
// UIColor+Color.swift
3+
// swift-cross-ui
4+
//
5+
// Created by William Baker on 1/10/25.
6+
//
7+
8+
import SwiftCrossUI
9+
import UIKit
10+
11+
extension UIColor {
12+
internal convenience init(color: Color) {
13+
self.init(
14+
red: CGFloat(color.red),
15+
green: CGFloat(color.green),
16+
blue: CGFloat(color.blue),
17+
alpha: CGFloat(color.alpha)
18+
)
19+
}
20+
21+
internal var swiftCrossUIColor: Color {
22+
let ciColor = CIColor(color: self)
23+
24+
return Color(
25+
Float(ciColor.red),
26+
Float(ciColor.green),
27+
Float(ciColor.blue),
28+
Float(ciColor.alpha)
29+
)
30+
}
31+
}
32+
33+
extension Color {
34+
internal init(_ uiColor: UIColor) {
35+
self = uiColor.swiftCrossUIColor
36+
}
37+
38+
internal var uiColor: UIColor {
39+
UIColor(color: self)
40+
}
41+
}

Sources/UIKitBackend/UIKitBackend+Container.swift

Lines changed: 20 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,17 @@ import UIKit
1010

1111
extension UIKitBackend {
1212
public func createContainer() -> Widget {
13-
UIView()
13+
BaseWidget()
1414
}
15-
15+
1616
public func removeAllChildren(of container: Widget) {
1717
container.subviews.forEach { $0.removeFromSuperview() }
1818
}
19-
19+
2020
public func addChild(_ child: Widget, to container: Widget) {
2121
container.addSubview(child)
22-
child.translatesAutoresizingMaskIntoConstraints = false
2322
}
24-
23+
2524
public func setPosition(
2625
ofChildAt index: Int,
2726
in container: Widget,
@@ -31,127 +30,43 @@ extension UIKitBackend {
3130
assertionFailure("Attempting to set position of nonexistent subview")
3231
return
3332
}
34-
35-
let child = container.subviews[index]
36-
37-
UIKitBackend.createOrUpdateConstraint(
38-
in: child,
39-
from: child.leftAnchor,
40-
to: container.leftAnchor,
41-
constant: CGFloat(position.x)
42-
)
43-
UIKitBackend.createOrUpdateConstraint(
44-
in: child,
45-
from: child.topAnchor,
46-
to: container.topAnchor,
47-
constant: CGFloat(position.y)
48-
)
33+
34+
let child = container.subviews[index] as! BaseWidget
35+
child.x = position.x
36+
child.y = position.y
4937
}
50-
38+
5139
public func removeChild(_ child: Widget, from container: Widget) {
5240
assert(child.isDescendant(of: container))
5341
child.removeFromSuperview()
5442
}
55-
43+
5644
public func createColorableRectangle() -> Widget {
57-
UIView()
45+
BaseWidget()
5846
}
59-
47+
6048
public func setColor(ofColorableRectangle widget: Widget, to color: Color) {
61-
let uiColor = UIColor(
62-
red: CGFloat(color.red),
63-
green: CGFloat(color.green),
64-
blue: CGFloat(color.blue),
65-
alpha: CGFloat(color.alpha)
66-
)
67-
widget.backgroundColor = uiColor
49+
widget.backgroundColor = color.uiColor
6850
}
69-
51+
7052
public func setCornerRadius(of widget: Widget, to radius: Int) {
7153
widget.layer.cornerRadius = CGFloat(radius)
7254
widget.layer.masksToBounds = true
7355
widget.setNeedsLayout()
7456
}
75-
57+
7658
public func naturalSize(of widget: Widget) -> SIMD2<Int> {
7759
let size = widget.intrinsicContentSize
7860
return SIMD2(
7961
Int(size.width),
8062
Int(size.height)
8163
)
8264
}
83-
65+
8466
public func setSize(of widget: Widget, to size: SIMD2<Int>) {
85-
widget.translatesAutoresizingMaskIntoConstraints = false
86-
UIKitBackend.createOrUpdateConstraint(
87-
in: widget,
88-
of: widget.widthAnchor,
89-
constant: CGFloat(size.x)
90-
)
91-
UIKitBackend.createOrUpdateConstraint(
92-
in: widget,
93-
of: widget.heightAnchor,
94-
constant: CGFloat(size.y)
95-
)
96-
}
97-
98-
public func createScrollContainer(for child: Widget) -> Widget {
99-
let scrollView = UIScrollView()
100-
scrollView.addSubview(child)
101-
102-
child.translatesAutoresizingMaskIntoConstraints = false
103-
UIKitBackend.createOrUpdateConstraint(
104-
in: child,
105-
from: child.leadingAnchor,
106-
to: scrollView.leadingAnchor,
107-
constant: 0.0
108-
)
109-
UIKitBackend.createOrUpdateConstraint(
110-
in: child,
111-
from: child.trailingAnchor,
112-
to: scrollView.trailingAnchor,
113-
constant: 0.0
114-
)
115-
UIKitBackend.createOrUpdateConstraint(
116-
in: child,
117-
from: child.topAnchor,
118-
to: scrollView.topAnchor,
119-
constant: 0.0
120-
)
121-
UIKitBackend.createOrUpdateConstraint(
122-
in: child,
123-
from: child.bottomAnchor,
124-
to: scrollView.bottomAnchor,
125-
constant: 0.0
126-
)
127-
128-
return scrollView
129-
}
130-
131-
internal static func createOrUpdateConstraint<Anchor: NSObject>(
132-
in view: UIView,
133-
from firstAnchor: NSLayoutAnchor<Anchor>,
134-
to secondAnchor: NSLayoutAnchor<Anchor>,
135-
constant: CGFloat
136-
) {
137-
if let constraint = view.constraints.first(where: {
138-
$0.firstAnchor === firstAnchor && $0.secondAnchor === secondAnchor
139-
}) {
140-
constraint.constant = constant
141-
} else {
142-
firstAnchor.constraint(equalTo: secondAnchor, constant: constant).isActive = true
143-
}
144-
}
145-
146-
internal static func createOrUpdateConstraint(
147-
in view: UIView,
148-
of anchor: NSLayoutDimension,
149-
constant: CGFloat
150-
) {
151-
if let constraint = view.constraints.first(where: { $0.firstAnchor === anchor }) {
152-
constraint.constant = constant
153-
} else {
154-
anchor.constraint(equalToConstant: constant).isActive = true
155-
}
67+
widget.width = size.x
68+
widget.height = size.y
15669
}
70+
71+
// TODO: Scroll containers
15772
}

0 commit comments

Comments
 (0)