Skip to content
This repository was archived by the owner on Dec 30, 2021. It is now read-only.

Commit 9df363d

Browse files
committed
Render ImageSet elements
1 parent e9eb14f commit 9df363d

File tree

19 files changed

+405
-2
lines changed

19 files changed

+405
-2
lines changed

Examples/AdaptiveCardVisualizer/AdaptiveCardVisualizer.xcodeproj/project.pbxproj

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

99
/* Begin PBXBuildFile section */
10+
82C107E8252F7CA60046764A /* Photos.json in Resources */ = {isa = PBXBuildFile; fileRef = 82C107E7252F7CA60046764A /* Photos.json */; };
11+
82C107E9252F7CA60046764A /* Photos.json in Resources */ = {isa = PBXBuildFile; fileRef = 82C107E7252F7CA60046764A /* Photos.json */; };
1012
9913B76B2514D5EC002C695C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9913B7562514D5EC002C695C /* Assets.xcassets */; };
1113
9913B76C2514D5EC002C695C /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9913B7562514D5EC002C695C /* Assets.xcassets */; };
1214
9913B7822514D695002C695C /* ActivityUpdate.json in Resources */ = {isa = PBXBuildFile; fileRef = 9913B7812514D695002C695C /* ActivityUpdate.json */; };
@@ -62,6 +64,7 @@
6264
/* End PBXBuildFile section */
6365

6466
/* Begin PBXFileReference section */
67+
82C107E7252F7CA60046764A /* Photos.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = Photos.json; sourceTree = "<group>"; };
6568
9913B7562514D5EC002C695C /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
6669
9913B75B2514D5EC002C695C /* AdaptiveCardVisualizer.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = AdaptiveCardVisualizer.app; sourceTree = BUILT_PRODUCTS_DIR; };
6770
9913B75E2514D5EC002C695C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
@@ -186,6 +189,7 @@
186189
9913B7D92514F18F002C695C /* WeatherCompact.json */,
187190
9913B7DE2514F458002C695C /* WeatherLarge.json */,
188191
9913B7ED2514F6A8002C695C /* FlightDetails.json */,
192+
82C107E7252F7CA60046764A /* Photos.json */,
189193
);
190194
path = Cards;
191195
sourceTree = "<group>";
@@ -311,6 +315,7 @@
311315
9913B78A2514D6B5002C695C /* prism.js in Resources */,
312316
9913B7D52514E065002C695C /* StockUpdate.json in Resources */,
313317
9913B7822514D695002C695C /* ActivityUpdate.json in Resources */,
318+
82C107E8252F7CA60046764A /* Photos.json in Resources */,
314319
9913B7DF2514F458002C695C /* WeatherLarge.json in Resources */,
315320
9913B78E2514D6B5002C695C /* okaidia.css in Resources */,
316321
9913B7CB2514DB63002C695C /* FlightUpdate.json in Resources */,
@@ -331,6 +336,7 @@
331336
9913B78B2514D6B5002C695C /* prism.js in Resources */,
332337
9913B7D62514E065002C695C /* StockUpdate.json in Resources */,
333338
9913B7832514D695002C695C /* ActivityUpdate.json in Resources */,
339+
82C107E9252F7CA60046764A /* Photos.json in Resources */,
334340
9913B7E02514F458002C695C /* WeatherLarge.json in Resources */,
335341
9913B78F2514D6B5002C695C /* okaidia.css in Resources */,
336342
9913B7CC2514DB63002C695C /* FlightUpdate.json in Resources */,
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
{
2+
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
3+
"type": "AdaptiveCard",
4+
"version": "1.0",
5+
"body": [
6+
{
7+
"type": "TextBlock",
8+
"text": "Here are some cool photos",
9+
"size": "large"
10+
},
11+
{
12+
"type": "TextBlock",
13+
"text": "from picsum.photos",
14+
"size": "medium",
15+
"weight": "lighter",
16+
"spacing": "none"
17+
},
18+
{
19+
"type": "ImageSet",
20+
"images": [
21+
{
22+
"type": "Image",
23+
"url": "https://picsum.photos/200/200?image=100",
24+
"altText": "White beach panorama"
25+
},
26+
{
27+
"type": "Image",
28+
"url": "https://picsum.photos/300/200?image=200",
29+
"altText": "Cow on a grassy field"
30+
},
31+
{
32+
"type": "Image",
33+
"url": "https://picsum.photos/300/200?image=301",
34+
"altText": "Orange leaves on the sidewalk of a park"
35+
},
36+
{
37+
"type": "Image",
38+
"url": "https://picsum.photos/200/200?image=400",
39+
"altText": "Green leaves"
40+
},
41+
{
42+
"type": "Image",
43+
"url": "https://picsum.photos/300/200?image=500",
44+
"altText": "Top of a sky scrapper"
45+
},
46+
{
47+
"type": "Image",
48+
"url": "https://picsum.photos/200/200?image=600",
49+
"altText": "Foggy forest"
50+
},
51+
{
52+
"type": "Image",
53+
"url": "https://picsum.photos/300/200?image=700",
54+
"altText": "Picure of the blue ocean"
55+
},
56+
{
57+
"type": "Image",
58+
"url": "https://picsum.photos/300/200?image=800",
59+
"altText": "Crowded train station"
60+
},
61+
{
62+
"type": "Image",
63+
"url": "https://picsum.photos/300/200?image=900",
64+
"altText": "Sunset under a dock"
65+
}
66+
]
67+
}
68+
]
69+
}

Examples/AdaptiveCardVisualizer/Shared/SampleCard.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,12 @@ extension SampleCard {
6161
resourceName: "FlightDetails.json"
6262
)
6363

64+
static let photos = SampleCard(
65+
id: "Photos",
66+
title: "Photos",
67+
resourceName: "Photos.json"
68+
)
69+
6470
static let all: [SampleCard] = [
6571
.gitHubRepository,
6672
.activityUpdate,
@@ -71,5 +77,6 @@ extension SampleCard {
7177
.weatherCompact,
7278
.weatherLarge,
7379
.flightDetails,
80+
.photos,
7481
]
7582
}

Sources/AdaptiveCardUI/Logic/FeatureAdaptation/CardElement+FeatureAdaptable.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ extension CardElement: FeatureAdaptable {
99
}
1010

1111
switch self {
12-
case .textBlock, .image, .richTextBlock, .factSet, .custom:
12+
case .textBlock, .image, .richTextBlock, .factSet, .imageSet, .custom:
1313
return self
1414
case var .actionSet(actionSet):
1515
var elementShouldFallback = false

Sources/AdaptiveCardUI/Logic/Store/AdaptiveCard+DuplicateIdentifiers.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ private extension CardElement {
2727
return [container.id] + container.items.flatMap(\.identifiers)
2828
case let .columnSet(columnSet):
2929
return [columnSet.id] + columnSet.columns.flatMap(\.identifiers)
30+
case let .imageSet(imageSet):
31+
return [imageSet.id] + imageSet.images.map(\.id)
3032
case .unknown:
3133
return []
3234
}

Sources/AdaptiveCardUI/Logic/Store/AdaptiveCard+ImageURLs.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ private extension CardElement {
2828
[container.backgroundImage?.url].compactMap { $0 }
2929
case let .columnSet(columnSet):
3030
return columnSet.columns.flatMap(\.imageURLs)
31+
case let .imageSet(imageSet):
32+
return imageSet.images.map(\.url)
3133
}
3234
}
3335
}

Sources/AdaptiveCardUI/Logic/ToggleVisibility/CardElement+Toggleable.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,13 @@ extension CardElement: Toggleable {
2121
} else {
2222
return false
2323
}
24+
case var .imageSet(imageSet):
25+
if imageSet.images.toggleVisibility(of: target) {
26+
self = .imageSet(imageSet)
27+
return true
28+
} else {
29+
return false
30+
}
2431
case .textBlock, .image, .richTextBlock, .actionSet, .factSet, .custom, .unknown:
2532
return false
2633
}
@@ -52,6 +59,9 @@ private extension CardElement {
5259
case var .factSet(element):
5360
element.toggleVisibility(isVisible)
5461
self = .factSet(element)
62+
case var .imageSet(element):
63+
element.toggleVisibility(isVisible)
64+
self = .imageSet(element)
5565
case var .custom(element):
5666
element.toggleVisibility(isVisible)
5767
self = .custom(element)
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import Foundation
2+
3+
extension Image: Toggleable {
4+
mutating func toggleVisibility(of target: TargetElement) -> Bool {
5+
guard id == target.elementId else { return false }
6+
7+
if let isVisible = target.isVisible {
8+
self.isVisible = isVisible
9+
} else {
10+
isVisible.toggle()
11+
}
12+
13+
return true
14+
}
15+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import DefaultCodable
2+
import Foundation
3+
4+
public enum MediumImageSize: DefaultValueProvider {
5+
public static var `default` = ImageSize.medium
6+
}
7+
8+
/// The `ImageSet` element displays a collection of images.
9+
public struct ImageSet: CardElementProtocol, Codable, Equatable {
10+
/// A unique identifier associated with the item.
11+
@ItemIdentifier public var id: String
12+
13+
/// If `false`, this item will be removed from the visual tree.
14+
@Default<True> public var isVisible: Bool
15+
16+
/// When `true`, draw a separating line at the top of the element.
17+
@Default<False> public var separator: Bool
18+
19+
/// Controls the amount of spacing between this element and the preceding element.
20+
@Default<FirstCase> public var spacing: Spacing
21+
22+
/// Describes what to do when an unknown element is encountered or the requires of this or any children can’t be met.
23+
@Default<Fallback.None> public var fallback: Fallback<CardElement>
24+
25+
/// A series of key/value pairs indicating features that the item requires with corresponding minimum version.
26+
/// When a feature is missing or of insufficient version, fallback is triggered.
27+
@Default<EmptyDictionary> public var requires: [String: SemanticVersion]
28+
29+
/// The array of `Image` elements to show.
30+
public var images: [Image]
31+
32+
/// Controls the approximate size of each image.
33+
///
34+
/// `auto` and `stretch` are not supported for ImageSet. The size will
35+
/// default to `medium` if those values are set.
36+
@Default<MediumImageSize> public var imageSize: ImageSize
37+
38+
public init(
39+
id: String = "",
40+
isVisible: Bool = true,
41+
separator: Bool = false,
42+
spacing: Spacing = .default,
43+
fallback: Fallback<CardElement> = .none,
44+
requires: [String: SemanticVersion] = [:],
45+
images: [Image],
46+
imageSize: ImageSize = .medium
47+
) {
48+
self.id = id
49+
self.isVisible = isVisible
50+
self.separator = separator
51+
self.spacing = spacing
52+
self.fallback = fallback
53+
self.requires = requires
54+
self.images = images
55+
self.imageSize = imageSize
56+
}
57+
}

Sources/AdaptiveCardUI/Model/Elements/CardElement.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ public indirect enum CardElement {
2424
/// A series of facts (i.e. name / value pairs) in a tabular form.
2525
case factSet(FactSet)
2626

27+
/// A collection of images.
28+
case imageSet(ImageSet)
29+
2730
/// A custom card element.
2831
case custom(CustomCardElement)
2932

@@ -56,6 +59,8 @@ extension CardElement: Codable {
5659
self = .columnSet(try ColumnSet(from: decoder))
5760
case String(describing: FactSet.self):
5861
self = .factSet(try FactSet(from: decoder))
62+
case String(describing: ImageSet.self):
63+
self = .imageSet(try ImageSet(from: decoder))
5964
default:
6065
if let decodeCustomCardElement = Self.customCardElementDecoders[type] {
6166
self = .custom(try decodeCustomCardElement(decoder))
@@ -90,6 +95,9 @@ extension CardElement: Codable {
9095
case let .factSet(element):
9196
try container.encode(String(describing: FactSet.self), forKey: .type)
9297
try element.encode(to: encoder)
98+
case let .imageSet(element):
99+
try container.encode(String(describing: ImageSet.self), forKey: .type)
100+
try element.encode(to: encoder)
93101
case let .custom(element):
94102
let typeName = type(of: element).typeName
95103
guard let encodeCustomCardElement = Self.customCardElementEncoders[typeName] else {
@@ -126,6 +134,8 @@ public extension CardElement {
126134
return element[keyPath: keyPath]
127135
case let .factSet(element):
128136
return element[keyPath: keyPath]
137+
case let .imageSet(element):
138+
return element[keyPath: keyPath]
129139
case let .custom(element):
130140
return element[keyPath: keyPath]
131141
case let .unknown(element):
@@ -174,6 +184,8 @@ extension CardElement: Equatable {
174184
return l == r
175185
case let (.factSet(l), .factSet(r)):
176186
return l == r
187+
case let (.imageSet(l), .imageSet(r)):
188+
return l == r
177189
case let (.custom(l), .custom(r)):
178190
let typeName = type(of: l).typeName
179191
return Self.customCardElementIsEqual[typeName]?(l, r) ?? false

Sources/AdaptiveCardUI/UI/CardElement/CardElementList.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@
6262
private extension CardElement {
6363
var bleed: Bool {
6464
switch self {
65-
case .textBlock, .image, .richTextBlock, .actionSet, .factSet, .custom:
65+
case .textBlock, .image, .richTextBlock, .actionSet, .factSet, .imageSet, .custom:
6666
return false
6767
case let .container(element):
6868
return element.bleed

Sources/AdaptiveCardUI/UI/CardElement/CardElementView.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626
ColumnSetView(columnSet)
2727
case let .factSet(factSet):
2828
FactSetView(factSet)
29+
case let .imageSet(imageSet):
30+
ImageSetView(imageSet)
2931
case let .custom(customCardElement):
3032
CustomCardElementView(customCardElement)
3133
default:
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
#if canImport(SwiftUI)
2+
3+
import SwiftUI
4+
5+
// Adapted from https://gist.github.com/chriseidhof/3c6ea3fb2102052d1898d8ea27fbee07
6+
@available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *)
7+
struct FlowLayout {
8+
private let proposedSize: CGSize
9+
private let horizontalSpacing: CGFloat
10+
private let verticalSpacing: CGFloat
11+
12+
private var position = CGPoint.zero
13+
private var lineHeight: CGFloat = 0
14+
15+
var size: CGSize {
16+
CGSize(width: proposedSize.width, height: position.y + lineHeight)
17+
}
18+
19+
init(proposedSize: CGSize, horizontalSpacing: CGFloat, verticalSpacing: CGFloat) {
20+
self.proposedSize = proposedSize
21+
self.horizontalSpacing = horizontalSpacing
22+
self.verticalSpacing = verticalSpacing
23+
}
24+
25+
mutating func addElementWithSize(_ size: CGSize) -> CGRect {
26+
if position.x + size.width > proposedSize.width {
27+
position.x = 0
28+
position.y += lineHeight + verticalSpacing
29+
lineHeight = 0
30+
}
31+
32+
let result = CGRect(origin: position, size: size)
33+
34+
lineHeight = max(lineHeight, size.height)
35+
position.x += size.width + horizontalSpacing
36+
37+
return result
38+
}
39+
}
40+
41+
#endif

0 commit comments

Comments
 (0)