Skip to content

Commit e70fb07

Browse files
committed
Add sheet like variation on swiftMessage modifier
1 parent f3555f0 commit e70fb07

9 files changed

+190
-25
lines changed

CHANGELOG.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,18 @@
11
# Change Log
22
All notable changes to this project will be documented in this file.
33

4+
## 9.0.10
5+
6+
### Features
7+
8+
* Add a `.sheet()` like variation to the `.swiftMessage()` modifier that takes a view builder. This provides more flexibility for constructing message views.
9+
10+
### Fixes
11+
12+
* #535 window being accessed from background thread when dequeueNext is called
13+
* #534 Xcode warnings in two swift files
14+
* #533 How do I show a message that appears above the keyboard, when the keyboard is already visible?
15+
416
## 9.0.9
517

618
### Fixes

README.md

Lines changed: 34 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -178,8 +178,7 @@ And check out our blog post [Elegant Custom UIViewController Transitioning](http
178178
179179
Any of the built-in SwiftMessages views can be displayed by calling the SwiftMessages APIs from within observable object, a button action closure, etc. However, SwiftMessages can also display your custom SwiftUI views.
180180
181-
The first step is to define a type that conforms to `MessageViewConvertible`. This would typically be a struct containing the message data to display:
182-
181+
Take the following message view and companion data model:
183182
184183
````swift
185184
struct DemoMessage: Identifiable {
@@ -189,12 +188,6 @@ struct DemoMessage: Identifiable {
189188
var id: String { title + body }
190189
}
191190
192-
extension DemoMessage: MessageViewConvertible {
193-
func asMessageView() -> DemoMessageView {
194-
DemoMessageView(message: self)
195-
}
196-
}
197-
198191
struct DemoMessageView: View {
199192
200193
let message: DemoMessage
@@ -212,22 +205,22 @@ struct DemoMessageView: View {
212205
// This makes a tab-style view where the bottom corners are rounded and
213206
// the view's background extends to the top edge.
214207
.mask(
215-
UnevenRoundedRectangle(bottomLeadingRadius: 15, bottomTrailingRadius: 15)
208+
UnevenRoundedRectangle(bottomLeadingRadius: 15, bottomTrailingRadius: 15)
216209
// This causes the background to extend into the safe area to the screen edge.
217210
.edgesIgnoringSafeArea(.top)
218211
)
219212
}
220213
}
221214
````
222215
223-
The SwiftUI message view can be displayed just like any other UIKit message by using `MessageHostingView`:
216+
You can show it from a button action, view model or other similar context like:
224217
225218
````swift
226219
struct DemoView: View {
227220
var body: some View {
228221
Button("Show message") {
229222
let message = DemoMessage(title: "Demo", body: "SwiftUI forever!")
230-
let messageView = MessageHostingView(message: message)
223+
let messageView = MessageHostingView(id: message.id, content: DemoMessageView(message: message)
231224
SwiftMessages.show(view: messageView)
232225
}
233226
}
@@ -245,12 +238,40 @@ struct DemoView: View {
245238
Button("Show message") {
246239
message = DemoMessage(title: "Demo", body: "SwiftUI forever!")
247240
}
248-
.swiftMessage(message: $message)
241+
.swiftMessage(message: $message) { message in
242+
DemoMessageView(message: message)
243+
}
249244
}
250245
}
251246
````
252247
253-
This technique may be more SwiftUI-like, but it doesn't offer the full capability of SwiftMessages, such as explicitly hiding messages by their ID. It is totally reasonable to use a combination of both approaches.
248+
This is very similar to the `.sheet()` modifier. However, it doesn't expose all of the features of SwiftMessages, such as explicitly hiding messages by ID. It is totally reasonable to use a combination of both approaches.
249+
250+
If your message views are purely data-driven and don't require delegates, callbacks, etc., there is a slightly simplified variation on `swiftMessage()` that doesn't require a view builder. Instead, your data model should conform to `MessageViewConvertible`.
251+
252+
````swift
253+
extension DemoMessage: MessageViewConvertible {
254+
func asMessageView() -> DemoMessageView {
255+
DemoMessageView(message: self)
256+
}
257+
}
258+
````
259+
260+
Then you can drop the view builder when calling `swiftMessage()`:
261+
262+
````swift
263+
struct DemoView: View {
264+
265+
@State var message: DemoMessage?
266+
267+
var body: some View {
268+
Button("Show message") {
269+
message = DemoMessage(title: "Demo", body: "SwiftUI forever!")
270+
}
271+
.swiftMessage(message: $message)
272+
}
273+
}
274+
````
254275
255276
Try it out in the SwiftUI demo app!
256277

SwiftMessages.podspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
Pod::Spec.new do |spec|
22
spec.name = 'SwiftMessages'
3-
spec.version = '9.0.9'
3+
spec.version = '9.0.10'
44
spec.license = { :type => 'MIT' }
55
spec.homepage = 'https://github.com/SwiftKickMobile/SwiftMessages'
66
spec.authors = { 'Timothy Moose' => 'tim@swiftkickmobile.com' }

SwiftMessages/MessageHostingView.swift

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,20 @@ public class MessageHostingView<Content>: BaseView, Identifiable where Content:
1616

1717
public let id: String
1818

19-
public init<Message>(message: Message) where Message: MessageViewConvertible, Message.Content == Content {
20-
let messageView: Content = message.asMessageView()
21-
hostVC = UIHostingController(rootView: messageView)
22-
id = message.id
19+
public init(id: String, content: Content) {
20+
hostVC = UIHostingController(rootView: content)
21+
self.id = id
2322
super.init(frame: .zero)
2423
hostVC.loadViewIfNeeded()
2524
installContentView(hostVC.view)
2625
backgroundColor = .clear
2726
hostVC.view.backgroundColor = .clear
2827
}
2928

29+
convenience public init<Message>(message: Message) where Message: MessageViewConvertible, Message.Content == Content {
30+
self.init(id: message.id, content: message.asMessageView() )
31+
}
32+
3033
// MARK: - Constants
3134

3235
// MARK: - Variables

SwiftMessages/SwiftMessageModifier.swift

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,28 +9,56 @@ import SwiftUI
99

1010
@available(iOS 14.0, *)
1111
public extension View {
12-
/// A state-based modifier for displaying a message.
12+
/// A state-based modifier for displaying a message when `Message` does not conform to `MessageViewConvertible`. This variant is more flexible and
13+
/// should be used if the message view can't be represented as pure data, such as if it requires a delegate, has callbacks, etc.
14+
func swiftMessage<Message, MessageContent>(
15+
message: Binding<Message?>,
16+
config: SwiftMessages.Config? = nil,
17+
swiftMessages: SwiftMessages? = nil,
18+
@ViewBuilder messageContent: @escaping (Message) -> MessageContent
19+
) -> some View where Message: Equatable & Identifiable, MessageContent: View {
20+
modifier(SwiftMessageModifier(message: message, config: config, swiftMessages: swiftMessages, messageContent: messageContent))
21+
}
22+
23+
/// A state-based modifier for displaying a message when `Message` conforms to `MessageViewConvertible`. This variant should be used if the message
24+
/// view can be represented as pure data. If the message requires a delegate, has callbacks, etc., consider using the variant that takes a message view builder.
1325
func swiftMessage<Message>(
1426
message: Binding<Message?>,
1527
config: SwiftMessages.Config? = nil,
1628
swiftMessages: SwiftMessages? = nil
1729
) -> some View where Message: MessageViewConvertible {
18-
modifier(SwiftMessageModifier(message: message, config: config, swiftMessages: swiftMessages))
30+
swiftMessage(message: message, config: config, swiftMessages: swiftMessages) { content in
31+
content.asMessageView()
32+
}
1933
}
2034
}
2135

2236
@available(iOS 14.0, *)
23-
private struct SwiftMessageModifier<Message>: ViewModifier where Message: MessageViewConvertible {
37+
private struct SwiftMessageModifier<Message, MessageContent>: ViewModifier where Message: Equatable & Identifiable, MessageContent: View {
38+
2439
// MARK: - API
2540

2641
fileprivate init(
2742
message: Binding<Message?>,
2843
config: SwiftMessages.Config? = nil,
29-
swiftMessages: SwiftMessages? = nil
44+
swiftMessages: SwiftMessages? = nil,
45+
@ViewBuilder messageContent: @escaping (Message) -> MessageContent
3046
) {
3147
_message = message
3248
self.config = config
3349
self.swiftMessages = swiftMessages
50+
self.messageContent = messageContent
51+
}
52+
53+
fileprivate init(
54+
message: Binding<Message?>,
55+
config: SwiftMessages.Config? = nil,
56+
swiftMessages: SwiftMessages? = nil
57+
) where Message: MessageViewConvertible, Message.Content == MessageContent {
58+
_message = message
59+
self.config = config
60+
self.swiftMessages = swiftMessages
61+
self.messageContent = { $0.asMessageView() }
3462
}
3563

3664
// MARK: - Constants
@@ -40,15 +68,16 @@ private struct SwiftMessageModifier<Message>: ViewModifier where Message: Messag
4068
@Binding private var message: Message?
4169
private let config: SwiftMessages.Config?
4270
private let swiftMessages: SwiftMessages?
71+
@ViewBuilder private let messageContent: (Message) -> MessageContent
4372

4473
// MARK: - Body
4574

4675
func body(content: Content) -> some View {
4776
content
48-
.onChange(of: message) { _ in
77+
.onChange(of: message) { message in
4978
if let message {
5079
let show: @MainActor (SwiftMessages.Config, UIView) -> Void = swiftMessages?.show(config:view:) ?? SwiftMessages.show(config:view:)
51-
let view = MessageHostingView(message: message)
80+
let view = MessageHostingView(id: message.id, content: messageContent(message))
5281
var config = config ?? swiftMessages?.defaultConfig ?? SwiftMessages.defaultConfig
5382
config.eventListeners.append { event in
5483
if case .didHide = event, event.id == self.message?.id {

SwiftUIDemo/SwiftUIDemo.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
objects = {
88

99
/* Begin PBXBuildFile section */
10+
22549DC02B55CFE8005E3E21 /* DemoMessageWithButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22549DBF2B55CFE8005E3E21 /* DemoMessageWithButtonView.swift */; };
1011
228F7DAD2ACF17E8006C9644 /* SwiftUIDemoApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 228F7DAC2ACF17E8006C9644 /* SwiftUIDemoApp.swift */; };
1112
228F7DAF2ACF17E8006C9644 /* DemoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 228F7DAE2ACF17E8006C9644 /* DemoView.swift */; };
1213
228F7DB12ACF17E9006C9644 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 228F7DB02ACF17E9006C9644 /* Assets.xcassets */; };
@@ -49,6 +50,7 @@
4950
/* End PBXCopyFilesBuildPhase section */
5051

5152
/* Begin PBXFileReference section */
53+
22549DBF2B55CFE8005E3E21 /* DemoMessageWithButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DemoMessageWithButtonView.swift; sourceTree = "<group>"; };
5254
228F7DA92ACF17E8006C9644 /* SwiftUIDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SwiftUIDemo.app; sourceTree = BUILT_PRODUCTS_DIR; };
5355
228F7DAC2ACF17E8006C9644 /* SwiftUIDemoApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftUIDemoApp.swift; sourceTree = "<group>"; };
5456
228F7DAE2ACF17E8006C9644 /* DemoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DemoView.swift; sourceTree = "<group>"; };
@@ -102,6 +104,7 @@
102104
228F7DAE2ACF17E8006C9644 /* DemoView.swift */,
103105
228F7DD42ACF59E4006C9644 /* DemoMessage.swift */,
104106
228F7DD62ACF5C2E006C9644 /* DemoMessageView.swift */,
107+
22549DBF2B55CFE8005E3E21 /* DemoMessageWithButtonView.swift */,
105108
228F7DB02ACF17E9006C9644 /* Assets.xcassets */,
106109
228F7DB22ACF17E9006C9644 /* Preview Content */,
107110
);
@@ -230,6 +233,7 @@
230233
228F7DD72ACF5C2E006C9644 /* DemoMessageView.swift in Sources */,
231234
228F7DAF2ACF17E8006C9644 /* DemoView.swift in Sources */,
232235
228F7DAD2ACF17E8006C9644 /* SwiftUIDemoApp.swift in Sources */,
236+
22549DC02B55CFE8005E3E21 /* DemoMessageWithButtonView.swift in Sources */,
233237
);
234238
runOnlyForDeploymentPostprocessing = 0;
235239
};

SwiftUIDemo/SwiftUIDemo/DemoMessageView.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
import SwiftUI
99

10-
// A card-style message view
10+
// A message view with a title and message.
1111
struct DemoMessageView: View {
1212

1313
// MARK: - API
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
//
2+
// DemoMessageWithButtonView.swift
3+
// SwiftUIDemo
4+
//
5+
// Created by Timothy Moose on 1/15/24.
6+
//
7+
8+
import SwiftUI
9+
10+
// A message view with a title, message and button.
11+
struct DemoMessageWithButtonView<Button>: View where Button: View {
12+
13+
// MARK: - API
14+
15+
enum Style {
16+
case standard
17+
case card
18+
case tab
19+
}
20+
21+
init(message: DemoMessage, style: Style, @ViewBuilder button: @escaping () -> Button) {
22+
self.message = message
23+
self.style = style
24+
self.button = button
25+
}
26+
27+
// MARK: - Variables
28+
29+
let message: DemoMessage
30+
let style: Style
31+
@ViewBuilder let button: () -> Button
32+
33+
// MARK: - Constants
34+
35+
// MARK: - Body
36+
37+
var body: some View {
38+
switch style {
39+
case .standard:
40+
content()
41+
// Mask the content and extend background into the safe area.
42+
.mask {
43+
Rectangle()
44+
.edgesIgnoringSafeArea(.top)
45+
}
46+
case .card:
47+
content()
48+
// Mask the content with a rounded rectangle
49+
.mask {
50+
RoundedRectangle(cornerRadius: 15)
51+
}
52+
// External padding around the card
53+
.padding(10)
54+
case .tab:
55+
content()
56+
// Mask the content with rounded bottom edge and extend background into the safe area.
57+
.mask {
58+
UnevenRoundedRectangle(bottomLeadingRadius: 15, bottomTrailingRadius: 15)
59+
.edgesIgnoringSafeArea(.top)
60+
}
61+
}
62+
}
63+
64+
@ViewBuilder private func content() -> some View {
65+
VStack() {
66+
Text(message.title).font(.system(size: 20, weight: .bold))
67+
Text(message.body)
68+
button()
69+
}
70+
.multilineTextAlignment(.center)
71+
// Internal padding of the card
72+
.padding(30)
73+
// Greedy width
74+
.frame(maxWidth: .infinity)
75+
.background(.demoMessageBackground)
76+
}
77+
}

SwiftUIDemo/SwiftUIDemo/DemoView.swift

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,12 @@ import SwiftMessages
1010

1111
struct DemoView: View {
1212

13+
/// Demonstrates purely data-driven message presentation.
1314
@State var message: DemoMessage?
1415

16+
/// Demonstrates message presentation with a view builder.
17+
@State var messageWithButton: DemoMessage?
18+
1519
var body: some View {
1620
VStack {
1721
Button("Show standard message") {
@@ -35,9 +39,24 @@ struct DemoView: View {
3539
style: .tab
3640
)
3741
}
42+
Button("Show message with button") {
43+
messageWithButton = DemoMessage(
44+
title: "Demo",
45+
body: "This message view has a button was constructed with a view builder.",
46+
style: .card
47+
)
48+
}
3849
}
3950
.buttonStyle(.bordered)
4051
.swiftMessage(message: $message)
52+
.swiftMessage(message: $messageWithButton) { message in
53+
DemoMessageWithButtonView(message: message, style: .card) {
54+
Button("Tap Me") {
55+
print("Tap")
56+
}
57+
.buttonStyle(.bordered)
58+
}
59+
}
4160
}
4261
}
4362

0 commit comments

Comments
 (0)