Skip to content

Commit 6c16e58

Browse files
authored
Add basic support for SwiftUI (#528)
1 parent 813938a commit 6c16e58

File tree

23 files changed

+910
-40
lines changed

23 files changed

+910
-40
lines changed

README.md

Lines changed: 90 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ SwiftMessages is a very flexible view and view controller presentation library f
1616

1717
Message views and view controllers can be displayed at the top, bottom, or center of the screen, or behind navigation bars and tab bars. There are interactive dismiss gestures including a fun, physics-based one. Multiple background dimming modes. And a lot more!
1818

19+
🔥 Now supports displaying SwiftUI message views 🔥
20+
1921
In addition to the numerous configuration options, SwiftMessages provides several good-looking layouts and themes. But SwiftMessages is also designer-friendly, which means you can fully and easily customize the view:
2022

2123
* Copy one of the included nib files into your project and change it.
@@ -32,19 +34,6 @@ Try exploring [the demo app via appetize.io](http://goo.gl/KXw4nD) to get a feel
3234
<a href="http://goo.gl/KXw4nD"><img src="./Demo/appetize.png" /></a>
3335
</p>
3436

35-
## View Controllers
36-
37-
SwiftMessages can present view controllers using the `SwiftMessagesSegue` custom modal segue!
38-
39-
<p align="center">
40-
<img src="./Design/SwiftMessagesSegue.gif" />
41-
</p>
42-
43-
[`SwiftMessagesSegue`](./SwiftMessages/SwiftMessagesSegue.swift) is a subclass of `UIStoryboardSegue` that integrates directly into Interface Builder as a custom modal segue, enabling view controllers to take advantage of SwiftMessages layouts, animations and more. `SwiftMessagesSegue` works with any UIKIt project — storyboards are not required. Refer to the View Controllers readme below for more information.
44-
45-
#### [View Controllers Readme](./ViewControllers.md)
46-
47-
And check out our blog post [Elegant Custom UIViewController Transitioning](http://www.swiftkickmobile.com/elegant-custom-uiviewcontroller-transitioning-uiviewcontrollertransitioningdelegate-uiviewcontrolleranimatedtransitioning/) to learn a great technique you can use to build your own custom segues that utilize `UIViewControllerTransitioningDelegate` and `UIViewControllerAnimatedTransitioning`.
4837

4938
## Installation
5039

@@ -178,6 +167,94 @@ config.duration = .forever
178167
SwiftMessages.show(config: config, view: view)
179168
````
180169
170+
### View Controllers
171+
172+
SwiftMessages can present view controllers using the `SwiftMessagesSegue` custom modal segue!
173+
174+
<p align="center">
175+
<img src="./Design/SwiftMessagesSegue.gif" />
176+
</p>
177+
178+
[`SwiftMessagesSegue`](./SwiftMessages/SwiftMessagesSegue.swift) is a subclass of `UIStoryboardSegue` that integrates directly into Interface Builder as a custom modal segue, enabling view controllers to take advantage of SwiftMessages layouts, animations and more. `SwiftMessagesSegue` works with any UIKIt project — storyboards are not required. Refer to the View Controllers readme below for more information.
179+
180+
#### [View Controllers Readme](./ViewControllers.md)
181+
182+
And check out our blog post [Elegant Custom UIViewController Transitioning](http://www.swiftkickmobile.com/elegant-custom-uiviewcontroller-transitioning-uiviewcontrollertransitioningdelegate-uiviewcontrolleranimatedtransitioning/) to learn a great technique you can use to build your own custom segues that utilize `UIViewControllerTransitioningDelegate` and `UIViewControllerAnimatedTransitioning`.
183+
184+
### SwiftUI
185+
186+
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.
187+
188+
First, define a type that conforms to `MessageViewConvertible`. This will typically be a struct containing the message data:
189+
190+
191+
````swift
192+
struct DemoMessage: Identifiable {
193+
let title: String
194+
let body: String
195+
196+
var id: String { title + body }
197+
}
198+
199+
extension DemoMessage: MessageViewConvertible {
200+
func asMessageView() -> DemoMessageView {
201+
DemoMessageView(message: self)
202+
}
203+
}
204+
205+
struct DemoMessageView: View {
206+
207+
let message: DemoMessage
208+
209+
var body: some View {
210+
VStack(alignment: .leading) {
211+
Text(message.title).font(.system(size: 20, weight: .bold))
212+
Text(message.body)
213+
}
214+
.multilineTextAlignment(.leading)
215+
.padding(30)
216+
.frame(maxWidth: .infinity)
217+
.background(.gray)
218+
.cornerRadius(15)
219+
.padding(15)
220+
}
221+
}
222+
````
223+
224+
The SwiftUI message view can be displayed just like any other UIKit message by using `MessageHostingView`:
225+
226+
````swift
227+
struct DemoView: View {
228+
var body: some View {
229+
Button("Show message") {
230+
let message = DemoMessage(title: "Demo", body: "SwiftUI forever!")
231+
let messageView = MessageHostingView(message: message)
232+
SwiftMessages.show(view: messageView)
233+
}
234+
}
235+
}
236+
````
237+
238+
But you may also use a state-based approach using the `swiftMessage()` view modifier:
239+
240+
````swift
241+
struct DemoView: View {
242+
243+
@State var message: DemoMessage?
244+
245+
var body: some View {
246+
Button("Show message") {
247+
message = DemoMessage(title: "Demo", body: "SwiftUI forever!")
248+
}
249+
.swiftMessage(message: $message)
250+
}
251+
}
252+
````
253+
254+
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.
255+
256+
Try it out in the SwiftUI demo app!
257+
181258
### Accessibility
182259
183260
SwiftMessages provides excellent VoiceOver support out-of-the-box.

SwiftMessages.podspec

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
Pod::Spec.new do |spec|
22
spec.name = 'SwiftMessages'
3-
spec.version = '9.0.6'
3+
spec.version = '9.0.7'
44
spec.license = { :type => 'MIT' }
55
spec.homepage = 'https://github.com/SwiftKickMobile/SwiftMessages'
66
spec.authors = { 'Timothy Moose' => 'tim@swiftkick.it' }
77
spec.summary = 'A very flexible message bar for iOS written in Swift.'
88
spec.source = {:git => 'https://github.com/SwiftKickMobile/SwiftMessages.git', :tag => spec.version}
9-
spec.platform = :ios, '9.0'
9+
spec.platform = :ios, '12.0'
1010
spec.swift_version = '5.0'
11-
spec.ios.deployment_target = '9.0'
11+
spec.ios.deployment_target = '12.0'
1212
spec.framework = 'UIKit'
1313
spec.requires_arc = true
1414
spec.default_subspec = 'App'

SwiftMessages.xcodeproj/project.pbxproj

Lines changed: 59 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
archiveVersion = 1;
44
classes = {
55
};
6-
objectVersion = 46;
6+
objectVersion = 54;
77
objects = {
88

99
/* Begin PBXBuildFile section */
@@ -52,6 +52,9 @@
5252
228DF5681FAD0806004F8A39 /* infoIconSubtle.png in Resources */ = {isa = PBXBuildFile; fileRef = 228DF5471FAD0805004F8A39 /* infoIconSubtle.png */; };
5353
228DF5691FAD0806004F8A39 /* successIconLight.png in Resources */ = {isa = PBXBuildFile; fileRef = 228DF5481FAD0805004F8A39 /* successIconLight.png */; };
5454
228DF56A1FAD0806004F8A39 /* infoIconSubtle@3x.png in Resources */ = {isa = PBXBuildFile; fileRef = 228DF5491FAD0805004F8A39 /* infoIconSubtle@3x.png */; };
55+
228F7DDE2ACF703A006C9644 /* MessageHostingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 228F7DDB2ACF7039006C9644 /* MessageHostingView.swift */; };
56+
228F7DDF2ACF703A006C9644 /* SwiftMessageModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 228F7DDC2ACF703A006C9644 /* SwiftMessageModifier.swift */; };
57+
228F7DE02ACF703A006C9644 /* MessageViewConvertible.swift in Sources */ = {isa = PBXBuildFile; fileRef = 228F7DDD2ACF703A006C9644 /* MessageViewConvertible.swift */; };
5558
2298C2051EE47DC900E2DDC1 /* Weak.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2298C2041EE47DC900E2DDC1 /* Weak.swift */; };
5659
2298C2071EE480D000E2DDC1 /* Animator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2298C2061EE480D000E2DDC1 /* Animator.swift */; };
5760
2298C2091EE486E300E2DDC1 /* TopBottomAnimation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2298C2081EE486E300E2DDC1 /* TopBottomAnimation.swift */; };
@@ -140,6 +143,9 @@
140143
228DF5471FAD0805004F8A39 /* infoIconSubtle.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = infoIconSubtle.png; path = Resources/infoIconSubtle.png; sourceTree = "<group>"; };
141144
228DF5481FAD0805004F8A39 /* successIconLight.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = successIconLight.png; path = Resources/successIconLight.png; sourceTree = "<group>"; };
142145
228DF5491FAD0805004F8A39 /* infoIconSubtle@3x.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; name = "infoIconSubtle@3x.png"; path = "Resources/infoIconSubtle@3x.png"; sourceTree = "<group>"; };
146+
228F7DDB2ACF7039006C9644 /* MessageHostingView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageHostingView.swift; sourceTree = "<group>"; };
147+
228F7DDC2ACF703A006C9644 /* SwiftMessageModifier.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwiftMessageModifier.swift; sourceTree = "<group>"; };
148+
228F7DDD2ACF703A006C9644 /* MessageViewConvertible.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MessageViewConvertible.swift; sourceTree = "<group>"; };
143149
2298C2041EE47DC900E2DDC1 /* Weak.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Weak.swift; sourceTree = "<group>"; };
144150
2298C2061EE480D000E2DDC1 /* Animator.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Animator.swift; sourceTree = "<group>"; };
145151
2298C2081EE486E300E2DDC1 /* TopBottomAnimation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TopBottomAnimation.swift; sourceTree = "<group>"; };
@@ -235,6 +241,15 @@
235241
name = Frameworks;
236242
sourceTree = "<group>";
237243
};
244+
228F7DDA2ACF7029006C9644 /* SwiftUI */ = {
245+
isa = PBXGroup;
246+
children = (
247+
228F7DDC2ACF703A006C9644 /* SwiftMessageModifier.swift */,
248+
228F7DDD2ACF703A006C9644 /* MessageViewConvertible.swift */,
249+
);
250+
name = SwiftUI;
251+
sourceTree = "<group>";
252+
};
238253
22D4779B20BF1C54005D0D71 /* View Controllers */ = {
239254
isa = PBXGroup;
240255
children = (
@@ -339,6 +354,7 @@
339354
86AAF8171D54F0650031EE32 /* PassthroughView.swift */,
340355
22E01F631E74EC8B00ACE19A /* MaskingView.swift */,
341356
86AAF8191D54F0850031EE32 /* PassthroughWindow.swift */,
357+
228F7DDB2ACF7039006C9644 /* MessageHostingView.swift */,
342358
220D38672597A94C00BB2B88 /* Extensions */,
343359
);
344360
name = Internal;
@@ -352,6 +368,7 @@
352368
862C0CD81D5A396900D06168 /* Resources */,
353369
2244656C1EF1D62700C50413 /* Animations */,
354370
22D4779B20BF1C54005D0D71 /* View Controllers */,
371+
228F7DDA2ACF7029006C9644 /* SwiftUI */,
355372
864495571D4F7C490056EB2A /* Base */,
356373
220D38682597A9FD00BB2B88 /* Extensions */,
357374
867E218E1D4D3DFD00594A41 /* Internal */,
@@ -434,8 +451,9 @@
434451
867E21471D4D01D500594A41 /* Project object */ = {
435452
isa = PBXProject;
436453
attributes = {
454+
BuildIndependentTargetsInParallel = YES;
437455
LastSwiftUpdateCheck = 0730;
438-
LastUpgradeCheck = 1200;
456+
LastUpgradeCheck = 1500;
439457
ORGANIZATIONNAME = "SwiftKick Mobile";
440458
TargetAttributes = {
441459
86B48AEB1D5A41C900063E2B = {
@@ -539,13 +557,16 @@
539557
86BBA9011D5E040600FE8F16 /* PassthroughWindow.swift in Sources */,
540558
2298C2071EE480D000E2DDC1 /* Animator.swift in Sources */,
541559
86BBA9031D5E040600FE8F16 /* UIViewController+Extensions.swift in Sources */,
560+
228F7DDF2ACF703A006C9644 /* SwiftMessageModifier.swift in Sources */,
542561
224FB69921153B440081D4DE /* CALayer+Extensions.swift in Sources */,
543562
22E01F641E74EC8B00ACE19A /* MaskingView.swift in Sources */,
544563
2298C2051EE47DC900E2DDC1 /* Weak.swift in Sources */,
564+
228F7DE02ACF703A006C9644 /* MessageViewConvertible.swift in Sources */,
545565
86BBA9001D5E040600FE8F16 /* PassthroughView.swift in Sources */,
546566
22DFC9181F00674E001B1CA1 /* PhysicsPanHandler.swift in Sources */,
547567
227BA6D920BF224A00E5A843 /* SwiftMessagesSegue.swift in Sources */,
548568
220655121FAF82B600F4E00F /* MarginAdjustable+Extensions.swift in Sources */,
569+
228F7DDE2ACF703A006C9644 /* MessageHostingView.swift in Sources */,
549570
22E307FF1E74C5B100E35893 /* AccessibleMessage.swift in Sources */,
550571
220D386E2597AA5B00BB2B88 /* SwiftMessages.Config+Extensions.swift in Sources */,
551572
2270044B1FAFA6DD0045DDC3 /* PhysicsAnimation.swift in Sources */,
@@ -619,6 +640,7 @@
619640
DEBUG_INFORMATION_FORMAT = dwarf;
620641
ENABLE_STRICT_OBJC_MSGSEND = YES;
621642
ENABLE_TESTABILITY = YES;
643+
ENABLE_USER_SCRIPT_SANDBOXING = YES;
622644
GCC_C_LANGUAGE_STANDARD = gnu99;
623645
GCC_DYNAMIC_NO_PIC = NO;
624646
GCC_NO_COMMON_BLOCKS = YES;
@@ -633,7 +655,7 @@
633655
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
634656
GCC_WARN_UNUSED_FUNCTION = YES;
635657
GCC_WARN_UNUSED_VARIABLE = YES;
636-
IPHONEOS_DEPLOYMENT_TARGET = 9.1;
658+
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
637659
MTL_ENABLE_DEBUG_INFO = YES;
638660
ONLY_ACTIVE_ARCH = YES;
639661
SDKROOT = iphoneos;
@@ -677,6 +699,7 @@
677699
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
678700
ENABLE_NS_ASSERTIONS = NO;
679701
ENABLE_STRICT_OBJC_MSGSEND = YES;
702+
ENABLE_USER_SCRIPT_SANDBOXING = YES;
680703
GCC_C_LANGUAGE_STANDARD = gnu99;
681704
GCC_NO_COMMON_BLOCKS = YES;
682705
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
@@ -685,10 +708,11 @@
685708
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
686709
GCC_WARN_UNUSED_FUNCTION = YES;
687710
GCC_WARN_UNUSED_VARIABLE = YES;
688-
IPHONEOS_DEPLOYMENT_TARGET = 9.1;
711+
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
689712
MTL_ENABLE_DEBUG_INFO = NO;
690713
SDKROOT = iphoneos;
691-
SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
714+
SWIFT_COMPILATION_MODE = wholemodule;
715+
SWIFT_OPTIMIZATION_LEVEL = "-O";
692716
SWIFT_VERSION = 3.0;
693717
VALIDATE_PRODUCT = YES;
694718
};
@@ -698,17 +722,24 @@
698722
isa = XCBuildConfiguration;
699723
buildSettings = {
700724
CLANG_ENABLE_MODULES = YES;
701-
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "";
725+
CODE_SIGN_IDENTITY = "";
702726
CURRENT_PROJECT_VERSION = 1;
703727
DEFINES_MODULE = YES;
704728
DYLIB_COMPATIBILITY_VERSION = 1;
705729
DYLIB_CURRENT_VERSION = 1;
706730
DYLIB_INSTALL_NAME_BASE = "@rpath";
731+
ENABLE_MODULE_VERIFIER = YES;
707732
GCC_WARN_ABOUT_DEPRECATED_FUNCTIONS = YES;
708733
INFOPLIST_FILE = SwiftMessages/Info.plist;
709734
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
710-
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
711-
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
735+
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
736+
LD_RUNPATH_SEARCH_PATHS = (
737+
"$(inherited)",
738+
"@executable_path/Frameworks",
739+
"@loader_path/Frameworks",
740+
);
741+
MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++";
742+
MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu99 gnu++11";
712743
PRODUCT_BUNDLE_IDENTIFIER = it.swiftkick.SwiftMessages;
713744
PRODUCT_NAME = "$(TARGET_NAME)";
714745
SKIP_INSTALL = YES;
@@ -726,17 +757,24 @@
726757
isa = XCBuildConfiguration;
727758
buildSettings = {
728759
CLANG_ENABLE_MODULES = YES;
729-
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "";
760+
CODE_SIGN_IDENTITY = "";
730761
CURRENT_PROJECT_VERSION = 1;
731762
DEFINES_MODULE = YES;
732763
DYLIB_COMPATIBILITY_VERSION = 1;
733764
DYLIB_CURRENT_VERSION = 1;
734765
DYLIB_INSTALL_NAME_BASE = "@rpath";
766+
ENABLE_MODULE_VERIFIER = YES;
735767
GCC_WARN_ABOUT_DEPRECATED_FUNCTIONS = YES;
736768
INFOPLIST_FILE = SwiftMessages/Info.plist;
737769
INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
738-
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
739-
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
770+
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
771+
LD_RUNPATH_SEARCH_PATHS = (
772+
"$(inherited)",
773+
"@executable_path/Frameworks",
774+
"@loader_path/Frameworks",
775+
);
776+
MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++";
777+
MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu99 gnu++11";
740778
PRODUCT_BUNDLE_IDENTIFIER = it.swiftkick.SwiftMessages;
741779
PRODUCT_NAME = "$(TARGET_NAME)";
742780
SKIP_INSTALL = YES;
@@ -753,7 +791,11 @@
753791
isa = XCBuildConfiguration;
754792
buildSettings = {
755793
INFOPLIST_FILE = SwiftMessagesTests/Info.plist;
756-
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
794+
LD_RUNPATH_SEARCH_PATHS = (
795+
"$(inherited)",
796+
"@executable_path/Frameworks",
797+
"@loader_path/Frameworks",
798+
);
757799
PRODUCT_BUNDLE_IDENTIFIER = it.swiftkick.SwiftMessagesTests;
758800
PRODUCT_NAME = "$(TARGET_NAME)";
759801
SWIFT_SWIFT3_OBJC_INFERENCE = On;
@@ -765,7 +807,11 @@
765807
isa = XCBuildConfiguration;
766808
buildSettings = {
767809
INFOPLIST_FILE = SwiftMessagesTests/Info.plist;
768-
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks";
810+
LD_RUNPATH_SEARCH_PATHS = (
811+
"$(inherited)",
812+
"@executable_path/Frameworks",
813+
"@loader_path/Frameworks",
814+
);
769815
PRODUCT_BUNDLE_IDENTIFIER = it.swiftkick.SwiftMessagesTests;
770816
PRODUCT_NAME = "$(TARGET_NAME)";
771817
SWIFT_SWIFT3_OBJC_INFERENCE = On;

SwiftMessages.xcodeproj/xcshareddata/xcschemes/SwiftMessages.xcscheme

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<?xml version="1.0" encoding="UTF-8"?>
22
<Scheme
3-
LastUpgradeVersion = "1200"
3+
LastUpgradeVersion = "1500"
44
version = "1.3">
55
<BuildAction
66
parallelizeBuildables = "YES"

SwiftMessages/BaseView.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -293,7 +293,7 @@ extension BaseView {
293293
/// because the background view may be masked. So, when modifying the drop shadow,
294294
/// be sure to set the shadow properties of this view's layer. The shadow path is
295295
/// updated for you automatically.
296-
open func configureDropShadow() {
296+
public func configureDropShadow() {
297297
layer.shadowColor = UIColor.black.cgColor
298298
layer.shadowOffset = CGSize(width: 0.0, height: 2.0)
299299
layer.shadowRadius = 6.0
@@ -303,7 +303,7 @@ extension BaseView {
303303
}
304304

305305
/// A convenience function to turn off drop shadow
306-
open func configureNoDropShadow() {
306+
public func configureNoDropShadow() {
307307
layer.shadowOpacity = 0
308308
}
309309

SwiftMessages/Identifiable.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import Foundation
1717
This protocol is optional. Message views that don't adopt `Identifiable` will not
1818
have duplicates removed.
1919
*/
20+
2021
public protocol Identifiable {
2122
var id: String { get }
2223
}

0 commit comments

Comments
 (0)