Skip to content

Commit 0de8f2d

Browse files
committed
Bump to swift-image-formats with debug mode optimisations, and only update Image views when their image source changes
Also updated strict frame modifiers to avoid unnecessarily getting the size of their child during dryRun updates when both their width and their height are specified.
1 parent 6738ff5 commit 0de8f2d

File tree

3 files changed

+104
-43
lines changed

3 files changed

+104
-43
lines changed

Package.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ let package = Package(
140140
),
141141
.package(
142142
url: "https://github.com/stackotter/swift-image-formats",
143-
.upToNextMinor(from: "0.1.0")
143+
.upToNextMinor(from: "0.1.1")
144144
),
145145
] + swift510Dependencies,
146146
targets: [

Sources/SwiftCrossUI/Modifiers/Frame.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,10 @@ struct StrictFrameView<Child: View>: TypeSafeView {
7070
backend: Backend,
7171
dryRun: Bool
7272
) -> ViewSize {
73+
if dryRun, let width, let height {
74+
return ViewSize(fixedSize: SIMD2(width, height))
75+
}
76+
7377
let proposedSize = SIMD2(
7478
width ?? proposedSize.x,
7579
height ?? proposedSize.y

Sources/SwiftCrossUI/Views/Image.swift

Lines changed: 99 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -2,41 +2,30 @@ import Foundation
22
import ImageFormats
33

44
/// A view that displays an image.
5-
public struct Image: View {
6-
private var image: ImageFormats.Image<RGBA>?
5+
public struct Image: TypeSafeView, View {
76
private var isResizable = false
7+
private var source: Source
88

9-
public var body: some View {
10-
if let image {
11-
_Image(image, resizable: isResizable)
12-
}
9+
enum Source: Equatable {
10+
case url(URL, useFileExtension: Bool)
11+
case image(ImageFormats.Image<RGBA>)
1312
}
1413

14+
public var body = EmptyView()
15+
1516
/// Displays an image file. `png`, `jpg`, and `webp` are supported.
1617
/// - Parameters:
1718
/// - url: The url of the file to display.
1819
/// - useFileExtension: If `true`, the file extension is used to determine the file type,
1920
/// otherwise the first few ('magic') bytes of the file are used.
2021
public init(_ url: URL, useFileExtension: Bool = true) {
21-
guard let data = try? Data(contentsOf: url) else {
22-
return
23-
}
24-
25-
let bytes = Array(data)
26-
if useFileExtension {
27-
image = try? ImageFormats.Image<RGBA>.load(
28-
from: bytes,
29-
usingFileExtension: url.pathExtension
30-
)
31-
} else {
32-
image = try? ImageFormats.Image<RGBA>.load(from: bytes)
33-
}
22+
source = .url(url, useFileExtension: useFileExtension)
3423
}
3524

3625
/// Displays an image from raw pixel data.
3726
/// - Parameter image: The image data to display.
3827
public init(_ image: ImageFormats.Image<RGBA>) {
39-
self.image = image
28+
source = .image(image)
4029
}
4130

4231
/// Makes the image resize to fit the available space.
@@ -45,59 +34,127 @@ public struct Image: View {
4534
image.isResizable = true
4635
return image
4736
}
48-
}
4937

50-
/// An internal implementation detail of ``Image``. Implements displaying of raw pixel data.
51-
struct _Image: ElementaryView, View {
52-
private var image: ImageFormats.Image<RGBA>
53-
private var resizable: Bool
38+
init(_ source: Source, resizable: Bool) {
39+
self.source = source
40+
self.isResizable = resizable
41+
}
42+
43+
func layoutableChildren<Backend: AppBackend>(
44+
backend: Backend,
45+
children: _ImageChildren
46+
) -> [LayoutSystem.LayoutableChild] {
47+
[]
48+
}
5449

55-
init(_ image: ImageFormats.Image<RGBA>, resizable: Bool) {
56-
self.image = image
57-
self.resizable = resizable
50+
func children<Backend: AppBackend>(
51+
backend: Backend,
52+
snapshots: [ViewGraphSnapshotter.NodeSnapshot]?,
53+
environment: Environment
54+
) -> _ImageChildren {
55+
_ImageChildren(backend: backend)
5856
}
5957

6058
func asWidget<Backend: AppBackend>(
59+
_ children: _ImageChildren,
6160
backend: Backend
6261
) -> Backend.Widget {
63-
return backend.createImageView()
62+
children.container.into()
6463
}
6564

6665
func update<Backend: AppBackend>(
6766
_ widget: Backend.Widget,
67+
children: _ImageChildren,
6868
proposedSize: SIMD2<Int>,
6969
environment: Environment,
7070
backend: Backend,
7171
dryRun: Bool
7272
) -> ViewSize {
73-
if !dryRun {
74-
backend.updateImageView(
75-
widget,
76-
rgbaData: image.data,
77-
width: image.width,
78-
height: image.height
79-
)
73+
let image: ImageFormats.Image<RGBA>?
74+
if source != children.cachedImageSource {
75+
switch source {
76+
case .url(let url, let useFileExtension):
77+
if let data = try? Data(contentsOf: url) {
78+
let bytes = Array(data)
79+
if useFileExtension {
80+
image = try? ImageFormats.Image<RGBA>.load(
81+
from: bytes,
82+
usingFileExtension: url.pathExtension
83+
)
84+
} else {
85+
image = try? ImageFormats.Image<RGBA>.load(from: bytes)
86+
}
87+
} else {
88+
image = nil
89+
}
90+
case .image(let sourceImage):
91+
image = sourceImage
92+
}
93+
94+
children.cachedImageSource = source
95+
children.cachedImage = image
96+
children.imageChanged = true
97+
} else {
98+
image = children.cachedImage
8099
}
81100

82-
let idealSize = SIMD2(image.width, image.height)
101+
let idealSize = SIMD2(image?.width ?? 0, image?.height ?? 0)
83102
let size: ViewSize
84-
if resizable {
103+
if isResizable {
85104
size = ViewSize(
86-
size: proposedSize,
105+
size: image == nil ? .zero : proposedSize,
87106
idealSize: idealSize,
88107
minimumWidth: 0,
89108
minimumHeight: 0,
90-
maximumWidth: nil,
91-
maximumHeight: nil
109+
maximumWidth: image == nil ? 0 : nil,
110+
maximumHeight: image == nil ? 0 : nil
92111
)
93112
} else {
94113
size = ViewSize(fixedSize: idealSize)
95114
}
96115

116+
if !dryRun && children.imageChanged {
117+
children.imageChanged = false
118+
if let image {
119+
backend.updateImageView(
120+
children.imageWidget.into(),
121+
rgbaData: image.data,
122+
width: image.width,
123+
height: image.height
124+
)
125+
if children.isContainerEmpty {
126+
backend.addChild(children.imageWidget.into(), to: children.container.into())
127+
backend.setPosition(ofChildAt: 0, in: children.container.into(), to: .zero)
128+
}
129+
children.isContainerEmpty = false
130+
} else {
131+
backend.removeAllChildren(of: children.container.into())
132+
children.isContainerEmpty = true
133+
}
134+
}
135+
97136
if !dryRun {
98-
backend.setSize(of: widget, to: size.size)
137+
backend.setSize(of: children.container.into(), to: size.size)
138+
backend.setSize(of: children.imageWidget.into(), to: size.size)
99139
}
100140

101141
return size
102142
}
103143
}
144+
145+
class _ImageChildren: ViewGraphNodeChildren {
146+
var cachedImageSource: Image.Source? = nil
147+
var cachedImage: ImageFormats.Image<RGBA>? = nil
148+
var container: AnyWidget
149+
var imageWidget: AnyWidget
150+
var imageChanged = false
151+
var isContainerEmpty = true
152+
153+
init<Backend: AppBackend>(backend: Backend) {
154+
container = AnyWidget(backend.createContainer())
155+
imageWidget = AnyWidget(backend.createImageView())
156+
}
157+
158+
var widgets: [AnyWidget] = []
159+
var erasedNodes: [ErasedViewGraphNode] = []
160+
}

0 commit comments

Comments
 (0)