Compare commits
28 Commits
Author | SHA1 | Date |
---|---|---|
![]() |
5534050e1d | |
![]() |
c09873eb81 | |
![]() |
c596673a9c | |
![]() |
14e9591d5c | |
![]() |
20b92ec4ec | |
![]() |
52c779254f | |
![]() |
2e2e6bcf3f | |
![]() |
9004d99344 | |
![]() |
29f602da95 | |
![]() |
9c07be248f | |
![]() |
bd2d812c4a | |
![]() |
8f1fd5f3fc | |
![]() |
77deb1269e | |
![]() |
6c743e5e3f | |
![]() |
ec5668cc75 | |
![]() |
5cf1908c6a | |
![]() |
4d780de447 | |
![]() |
82d4f74658 | |
![]() |
8948af3260 | |
![]() |
e073ba8b40 | |
![]() |
6fada57021 | |
![]() |
bbbfbcb7c5 | |
![]() |
903a063257 | |
![]() |
5c147debcd | |
![]() |
16d426d682 | |
![]() |
dc142d143f | |
![]() |
cad6d82289 | |
![]() |
dfe3deae3b |
Binary file not shown.
After Width: | Height: | Size: 926 KiB |
|
@ -17,6 +17,7 @@
|
|||
B69AAB4529F31E6700026789 /* FutureFlow in Frameworks */ = {isa = PBXBuildFile; productRef = B69AAB4429F31E6700026789 /* FutureFlow */; };
|
||||
B69AAB4729F42F8900026789 /* Demo2.swift in Sources */ = {isa = PBXBuildFile; fileRef = B69AAB4629F42F8900026789 /* Demo2.swift */; };
|
||||
B69AAB4929F44D2A00026789 /* Demo10.swift in Sources */ = {isa = PBXBuildFile; fileRef = B69AAB4829F44D2A00026789 /* Demo10.swift */; };
|
||||
B6B551EE29FDCB6E006CA32D /* README.md in Resources */ = {isa = PBXBuildFile; fileRef = B6B551ED29FDCB6E006CA32D /* README.md */; };
|
||||
B6C55E9729F5C75200E9E281 /* Demo11.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6C55E9629F5C75200E9E281 /* Demo11.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
|
@ -38,6 +39,8 @@
|
|||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
B643355129FDD16500E10D0E /* FutureFlow */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FutureFlow; path = ../..; sourceTree = "<group>"; };
|
||||
B643355229FDD35700E10D0E /* FutureFlow */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FutureFlow; path = ../..; sourceTree = "<group>"; };
|
||||
B69AAB1429F31CF300026789 /* FutureFlowDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = FutureFlowDemo.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
B69AAB1729F31CF300026789 /* FutureFlowDemoApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FutureFlowDemoApp.swift; sourceTree = "<group>"; };
|
||||
B69AAB1929F31CF300026789 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
|
||||
|
@ -50,6 +53,8 @@
|
|||
B69AAB3429F31CF500026789 /* FutureFlowDemoUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FutureFlowDemoUITestsLaunchTests.swift; sourceTree = "<group>"; };
|
||||
B69AAB4629F42F8900026789 /* Demo2.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Demo2.swift; sourceTree = "<group>"; };
|
||||
B69AAB4829F44D2A00026789 /* Demo10.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Demo10.swift; sourceTree = "<group>"; };
|
||||
B6A3C7F729FEF19000FC384C /* FutureFlow */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FutureFlow; path = ../..; sourceTree = "<group>"; };
|
||||
B6B551ED29FDCB6E006CA32D /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
|
||||
B6C55E9629F5C75200E9E281 /* Demo11.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Demo11.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
|
@ -79,9 +84,20 @@
|
|||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
B643355029FDD16500E10D0E /* Packages */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
B643355129FDD16500E10D0E /* FutureFlow */,
|
||||
B643355229FDD35700E10D0E /* FutureFlow */,
|
||||
B6A3C7F729FEF19000FC384C /* FutureFlow */,
|
||||
);
|
||||
name = Packages;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
B69AAB0B29F31CF300026789 = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
B643355029FDD16500E10D0E /* Packages */,
|
||||
B69AAB1629F31CF300026789 /* FutureFlowDemo */,
|
||||
B69AAB2729F31CF500026789 /* FutureFlowDemoTests */,
|
||||
B69AAB3129F31CF500026789 /* FutureFlowDemoUITests */,
|
||||
|
@ -103,13 +119,11 @@
|
|||
B69AAB1629F31CF300026789 /* FutureFlowDemo */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
B6B551EC29FDCB4E006CA32D /* NotRelated */,
|
||||
B69AAB1729F31CF300026789 /* FutureFlowDemoApp.swift */,
|
||||
B69AAB1929F31CF300026789 /* ContentView.swift */,
|
||||
B69AAB4629F42F8900026789 /* Demo2.swift */,
|
||||
B69AAB1D29F31CF400026789 /* Preview Content */,
|
||||
B69AAB1B29F31CF400026789 /* Assets.xcassets */,
|
||||
B69AAB4829F44D2A00026789 /* Demo10.swift */,
|
||||
B6C55E9629F5C75200E9E281 /* Demo11.swift */,
|
||||
);
|
||||
path = FutureFlowDemo;
|
||||
sourceTree = "<group>";
|
||||
|
@ -146,6 +160,17 @@
|
|||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
B6B551EC29FDCB4E006CA32D /* NotRelated */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
B69AAB4829F44D2A00026789 /* Demo10.swift */,
|
||||
B6C55E9629F5C75200E9E281 /* Demo11.swift */,
|
||||
B69AAB4629F42F8900026789 /* Demo2.swift */,
|
||||
B6B551ED29FDCB6E006CA32D /* README.md */,
|
||||
);
|
||||
path = NotRelated;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
|
@ -253,6 +278,7 @@
|
|||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
B6B551EE29FDCB6E006CA32D /* README.md in Resources */,
|
||||
B69AAB1F29F31CF400026789 /* Preview Assets.xcassets in Resources */,
|
||||
B69AAB1C29F31CF400026789 /* Assets.xcassets in Resources */,
|
||||
);
|
|
@ -0,0 +1,317 @@
|
|||
//
|
||||
// ContentView.swift
|
||||
// FutureFlowDemo
|
||||
//
|
||||
// Created by Muhand Jumah on 4/21/23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import FutureFlow
|
||||
|
||||
struct ParentWrapper: InstructionsView {
|
||||
@Binding var tutorial: TutorialChunks
|
||||
let next: () -> ()
|
||||
let back: () -> ()
|
||||
|
||||
@State private var instructions: [String] = []
|
||||
@Namespace var ns
|
||||
|
||||
var body: some View {
|
||||
RectanglesTutorial(instructions: self.$instructions)
|
||||
.onDone({ withNext in
|
||||
if(withNext) {
|
||||
self.next()
|
||||
} else {
|
||||
self.back()
|
||||
}
|
||||
})
|
||||
.animation(.easeOut, value: self.tutorial)
|
||||
.onAppear {
|
||||
switch self.tutorial {
|
||||
case .red1:
|
||||
self.instructions = ["This is red"]
|
||||
break
|
||||
case .red2:
|
||||
self.instructions = ["Red is purple now??? WHAT?"]
|
||||
break
|
||||
case .red3:
|
||||
self.instructions = ["No way, IT IS BLUE!!"]
|
||||
break
|
||||
case .yellow:
|
||||
self.instructions = ["This is yellow"]
|
||||
break
|
||||
case .green:
|
||||
self.instructions = ["This is green"]
|
||||
break
|
||||
}
|
||||
|
||||
}
|
||||
.onChange(of: self.tutorial) { newValue in
|
||||
switch newValue {
|
||||
case .red1:
|
||||
self.instructions = ["This is red"]
|
||||
break
|
||||
case .red2:
|
||||
self.instructions = ["Red is purple now??? WHAT?"]
|
||||
break
|
||||
case .red3:
|
||||
self.instructions = ["No way, IT IS BLUE!!"]
|
||||
break
|
||||
case .yellow:
|
||||
self.instructions = ["This is yellow"]
|
||||
break
|
||||
case .green:
|
||||
self.instructions = ["This is green"]
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum TutorialChunks: FlowChunk, CaseIterable {
|
||||
case red1
|
||||
case red2
|
||||
case red3
|
||||
case green
|
||||
case yellow
|
||||
|
||||
var spotlightShape: SpotlightShape {
|
||||
switch self {
|
||||
case .yellow:
|
||||
return .circle()
|
||||
default:
|
||||
return .rectangle()
|
||||
}
|
||||
}
|
||||
|
||||
var spotlightBackground: SpotlightBackground {
|
||||
switch self {
|
||||
case .green:
|
||||
return .ultraThinMaterial
|
||||
default:
|
||||
return .black
|
||||
}
|
||||
}
|
||||
|
||||
var instructionsViewPosition: InstructionsViewPosition? {
|
||||
switch self {
|
||||
case .red1, .red2, .red3:
|
||||
return .below
|
||||
case .green:
|
||||
return .above
|
||||
case .yellow:
|
||||
return .above
|
||||
}
|
||||
}
|
||||
|
||||
func instructionsView(_ next: @escaping () -> (), _ back: @escaping () -> ()) -> FutureFlow.AnyInstructionsView? {
|
||||
switch self {
|
||||
case .red1, .red2:
|
||||
ParentWrapper(tutorial:
|
||||
.init(
|
||||
get: {
|
||||
self
|
||||
|
||||
}, set: { _ in }
|
||||
),
|
||||
next: next,
|
||||
back: back
|
||||
)
|
||||
default:
|
||||
ParentWrapper(tutorial:
|
||||
.init(
|
||||
get: {
|
||||
self
|
||||
|
||||
}, set: { _ in }
|
||||
),
|
||||
next: next,
|
||||
back: back
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func hash(into hasher: inout Hasher) {
|
||||
switch self {
|
||||
case .red1, .red2, .red3:
|
||||
hasher.combine(0)
|
||||
case .green:
|
||||
hasher.combine(1)
|
||||
case .yellow:
|
||||
hasher.combine(2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ContentView: View {
|
||||
@State private var rectColor: Color = .red
|
||||
private var uniqueIdentifier: String = UUID().uuidString
|
||||
|
||||
@State private var showTutorial: Bool = true
|
||||
@Namespace var namespace
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 50) {
|
||||
Rectangle()
|
||||
.frame(width: 256, height: 256, alignment: .center)
|
||||
.foregroundColor(self.rectColor)
|
||||
.configureChunkForSpotlight(
|
||||
namespace: self.namespace,
|
||||
parentIdentifier: self.uniqueIdentifier,
|
||||
chunk: TutorialChunks.red1
|
||||
)
|
||||
|
||||
Rectangle()
|
||||
.frame(width: 64, height: 128, alignment: .center)
|
||||
.foregroundColor(Color.green)
|
||||
.configureChunkForSpotlight(
|
||||
namespace: self.namespace,
|
||||
parentIdentifier: self.uniqueIdentifier,
|
||||
chunk: TutorialChunks.green
|
||||
)
|
||||
|
||||
Circle()
|
||||
.frame(width: 128, height: 128, alignment: .center)
|
||||
.foregroundColor(.yellow)
|
||||
.configureChunkForSpotlight(
|
||||
namespace: self.namespace,
|
||||
parentIdentifier: self.uniqueIdentifier,
|
||||
chunk: TutorialChunks.yellow
|
||||
)
|
||||
.onTapGesture {
|
||||
// withAnimation(.linear) {
|
||||
self.showTutorial = true
|
||||
// }
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.assembleSpotlightChunks(namespace:self.namespace, uniqueIdentifier: self.uniqueIdentifier, chunks: Array(TutorialChunks.allCases), showTutorial: self.$showTutorial) {
|
||||
chunk in
|
||||
withAnimation(.easeOut) {
|
||||
switch chunk {
|
||||
case .red2:
|
||||
self.rectColor = .purple
|
||||
break
|
||||
case .red3:
|
||||
self.rectColor = .blue
|
||||
break
|
||||
default:
|
||||
self.rectColor = .red
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ContentView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ContentView()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
internal struct RectanglesTutorial: InstructionsView {
|
||||
@Binding private var instructions: [String]
|
||||
|
||||
@Namespace var positionNamespace
|
||||
private var positionId: String = "steps-position-id"
|
||||
@State private var showNext: Bool = true
|
||||
@State private var showPrevious: Bool = true
|
||||
@State private var currentStep: Int = 0
|
||||
|
||||
private var onDone: ((_ withNext: Bool) -> ())?
|
||||
|
||||
internal init(instructions: Binding<[String]>) {
|
||||
self._instructions = instructions
|
||||
// self.onDone = { _ in }
|
||||
self.onDone = nil
|
||||
}
|
||||
|
||||
fileprivate init(instructions: Binding<[String]>, onDone: @escaping (_ withNext: Bool) -> ()) {
|
||||
self._instructions = instructions
|
||||
self.onDone = onDone
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
HStack(alignment: .top, spacing: 10) {
|
||||
Image(systemName: "lightbulb.circle.fill")
|
||||
.font(.system(size: 32))
|
||||
.foregroundColor(.blue)
|
||||
if(self.currentStep < self.instructions.count) {
|
||||
Text(self.instructions[self.currentStep])
|
||||
.font(.body)
|
||||
.animation(.easeOut, value: self.currentStep)
|
||||
}
|
||||
}
|
||||
.matchedGeometryEffect(id: positionId, in: self.positionNamespace, properties: .position, anchor: .bottomTrailing, isSource: true)
|
||||
|
||||
HStack(alignment: .center, spacing:15) {
|
||||
if(self.showPrevious) {
|
||||
Button(action: {
|
||||
guard self.currentStep > 0 else {
|
||||
if let onDone = self.onDone {
|
||||
onDone(false)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
self.currentStep -= 1
|
||||
}) {
|
||||
Text("Previous")
|
||||
.font(.callout)
|
||||
.fontWeight(.medium)
|
||||
.foregroundColor(.blue)
|
||||
}
|
||||
}
|
||||
|
||||
if(self.showNext) {
|
||||
Button(action: {
|
||||
guard self.currentStep < self.instructions.count - 1 else {
|
||||
if let onDone = self.onDone {
|
||||
onDone(true)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
self.currentStep += 1
|
||||
}) {
|
||||
Text("Next")
|
||||
.font(.callout)
|
||||
.fontWeight(.semibold)
|
||||
.padding(.horizontal, 10)
|
||||
.padding(.vertical, 5)
|
||||
.background(Color.blue)
|
||||
.cornerRadius(3)
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
}
|
||||
}
|
||||
.transition(.slide)
|
||||
.animation(.linear, value: self.showNext)
|
||||
.animation(.linear, value: self.showPrevious)
|
||||
.padding(.top, 10)
|
||||
.matchedGeometryEffect(id: positionId, in: self.positionNamespace, properties: .position, anchor: .topTrailing, isSource: false)
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
.padding(.vertical, 15)
|
||||
.background(Color.white)
|
||||
.cornerRadius(10)
|
||||
.shadow(color: .black.opacity(0.2), radius: 5, x: 0, y: 0)
|
||||
.animation(.easeOut, value: self.currentStep)
|
||||
.onAppear {
|
||||
self.currentStep = 0
|
||||
}
|
||||
.onChange(of: self.instructions) { newV in
|
||||
self.currentStep = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal extension RectanglesTutorial {
|
||||
func onDone(_ callback: @escaping (_ withNext: Bool) -> ()) -> RectanglesTutorial {
|
||||
.init(instructions: self.$instructions, onDone: callback)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,216 @@
|
|||
//////
|
||||
////// Demo11.swift
|
||||
////// FutureFlowDemo
|
||||
//////
|
||||
////// Created by Muhand Jumah on 4/23/23.
|
||||
//////
|
||||
////
|
||||
////import SwiftUI
|
||||
////
|
||||
////struct Demo11: View {
|
||||
//// var body: some View {
|
||||
//// ScrollView {
|
||||
//// Text (
|
||||
//// "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec et orci metus. Donec consectetur laoreet finibus. Pellentesque at maximus leo. Donec nec elementum metus, ut interdum libero. Cras hendrerit neque non magna sodales, eu lacinia justo laoreet. Phasellus pharetra vehicula nibh, porttitor elementum velit vestibulum id. Pellentesque quis mattis risus, eget feugiat justo. Maecenas a tortor neque. Nullam placerat non mauris eget tristique. Nam auctor dolor sit amet tincidunt pulvinar.\n\nNunc cursus eget quam ut convallis. In hac habitasse platea dictumst. Integer id elit ac tellus vehicula viverra eu ac metus. Cras tempus nisl eget nulla porttitor, placerat sollicitudin justo tincidunt. Phasellus et leo mi. Curabitur convallis malesuada mi tincidunt scelerisque. Sed rutrum ultricies ligula. Pellentesque nisl metus, euismod venenatis dui sit amet, lobortis lacinia quam. Nullam ipsum est, tempus et nisl nec, consectetur fermentum nulla. Fusce velit augue, consectetur nec purus id, maximus bibendum elit. Phasellus ornare odio sit amet erat tristique dapibus. Aliquam erat volutpat. Nam hendrerit diam eget arcu dapibus lacinia. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed ligula augue, aliquam nec condimentum quis, consectetur a erat.\nFusce at ipsum leo. Morbi egestas sem sit amet velit vestibulum gravida. Nullam eu finibus lorem. Nam ut lacus turpis. Nullam eu sagittis dui. Ut et condimentum purus. Sed et lectus pretium, imperdiet nulla sit amet, pulvinar enim. Suspendisse blandit tempus varius. Sed at consequat orci. Quisque feugiat malesuada mauris nec bibendum. Aenean condimentum ornare leo, quis porttitor risus malesuada vitae. Duis interdum odio ut scelerisque aliquet. Nam et viverra felis. Nulla eu nisi eu nisi condimentum bibendum at in odio. Aenean ut arcu quam.\nInteger non tristique turpis. Vestibulum non dolor metus. Phasellus odio lorem, consequat quis fermentum nec, faucibus scelerisque orci. Sed id placerat nibh, quis ultrices diam. Sed laoreet quis massa vel tincidunt. Proin maximus accumsan convallis. Morbi sed mi ac turpis condimentum faucibus. Donec eros nunc, dignissim vitae nibh a, pulvinar suscipit mi. Nam libero odio, rhoncus id eleifend sed, porttitor ut metus. Nunc fringilla eget ligula vel dictum. Aenean eget posuere eros. Sed placerat nec sem at faucibus. Sed vehicula sollicitudin lectus, ut consequat quam commodo in.\nIn id facilisis ligula, ut aliquet urna. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Nullam dictum blandit metus vitae dignissim. Nunc ac orci sed lorem volutpat tincidunt. Nulla vel purus nunc. Duis rutrum lorem et semper scelerisque. Quisque rutrum lorem eu arcu ultricies semper. Mauris auctor vulputate est non aliquam. Vivamus viverra ullamcorper sem, et consectetur est dapibus ut. Aliquam at massa sed lectus hendrerit aliquam in et mi. Sed venenatis tincidunt sem, ut rutrum augue vestibulum vitae. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Phasellus hendrerit quam et erat finibus tempor. Donec vehicula ut mi non ullamcorper. Sed lorem elit, hendrerit eu volutpat ut, tincidunt vel leo. "
|
||||
//// )
|
||||
//// .fixedSize(horizontal: false, vertical: true)
|
||||
//// .frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
//// }.frame(maxHeight: .infinity).padding()
|
||||
//// }
|
||||
////}
|
||||
////
|
||||
////struct Demo11_Previews: PreviewProvider {
|
||||
//// static var previews: some View {
|
||||
//// Demo11()
|
||||
//// }
|
||||
////}
|
||||
//
|
||||
//import SwiftUI
|
||||
//@available(iOS 16.0, *)
|
||||
//extension Image {
|
||||
// @MainActor func frame(width: CGFloat, height: CGFloat) -> Image {
|
||||
// let imageRenderer: ImageRenderer = .init(
|
||||
// content:
|
||||
// self
|
||||
// .resizable()
|
||||
// .frame(width: width, height: height, alignment: .center)
|
||||
// )
|
||||
//
|
||||
// if let uiImage = imageRenderer.uiImage {
|
||||
// return Image(uiImage: uiImage)
|
||||
// } else {
|
||||
// return
|
||||
// Image(systemName: "x.circle")
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//@available(iOS 16.0, *)
|
||||
//struct Demo11: View {
|
||||
// var body: some View {
|
||||
//// ScrollView(.vertical) {
|
||||
//// VStack(alignment: .leading, spacing: 10) {
|
||||
//// HStack(alignment: .top, spacing: 10) {
|
||||
//// Image(systemName: "square.fill")
|
||||
//// .resizable()
|
||||
//// .frame(width: 80, height: 80)
|
||||
//// .foregroundColor(.black)
|
||||
////
|
||||
//// Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed fermentum vulputate euismod. In facilisis diam et mollis consequat jkahdfk ladshfkljahsd fjlkahsd fjlkhasd kjlhasdjk faljk dsfljah sdfjlk hasdklj asdnf msdnf m,.asnf asdjhf ljaskhf lkjasdh fljakshd fljkash. ")
|
||||
//// }
|
||||
////
|
||||
//// Text("Ut vulputate ultrices erat, sit amet auctor felis tristique vel. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Morbi felis leo, blandit cursus tortor eu, ullamcorper volutpat orci. Donec ornare velit sed neque scelerisque, sit amet malesuada nunc laoreet. Ut non lobortis felis, vel ultrices ligula. Nam massa mauris, volutpat et felis lacinia, dignissim efficitur arcu. Vestibulum euismod in risus at consequat. Nunc a vestibulum dolor. Vivamus sed augue lectus. Duis felis leo, gravida quis est ac, volutpat suscipit nulla. Pellentesque pharetra vulputate erat ut lobortis. Vivamus sed nibh enim. Phasellus pharetra malesuada justo quis molestie. Sed eget lectus at turpis elementum posuere quis ut velit. Morbi suscipit venenatis tempus. Ut sed arcu ipsum. ")
|
||||
//// }
|
||||
//// }
|
||||
//// .padding()
|
||||
//
|
||||
// ScrollView(.vertical) {
|
||||
//// Text("Ut vulputate ultrices erat, sit amet auctor felis tristique vel. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Morbi felis leo, blandit cursus tortor eu, ullamcorper volutpat orci. Donec ornare velit sed neque scelerisque, sit amet malesuada nunc laoreet. Ut non lobortis felis, vel ultrices ligula. Nam massa mauris, volutpat et felis lacinia") + Text(Image(systemName: "square.fill")) + Text(", dignissim efficitur arcu. Vestibulum euismod in risus at consequat. Nunc a vestibulum dolor. Vivamus sed augue lectus. Duis felis leo, gravida quis est ac, volutpat suscipit nulla. Pellentesque pharetra vulputate erat ut lobortis. Vivamus sed nibh enim. Phasellus pharetra malesuada justo quis molestie. Sed eget lectus at turpis elementum posuere quis ut velit. Morbi suscipit venenatis tempus. Ut sed arcu ipsum.")
|
||||
// AsyncImage(url: .init(string: "https://1000logos.net/wp-content/uploads/2016/10/Apple-Logo.png")) { img in
|
||||
//// Text("Ut vulputate ultrices erat, sit amet auctor felis tristique vel. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Morbi felis leo, blandit cursus tortor eu, ullamcorper volutpat orci. Donec ornare velit sed neque scelerisque, sit amet malesuada nunc laoreet. Ut non lobortis felis, vel ultrices ligula. Nam massa mauris, volutpat et felis lacinia") +
|
||||
////// Text(img.frame(width: 80, height: 80)) +
|
||||
//// Text(Image(systemName: "x.circle")) +
|
||||
//// Text(", dignissim efficitur arcu. Vestibulum euismod in risus at consequat. Nunc a vestibulum dolor. Vivamus sed augue lectus. Duis felis leo, gravida quis est ac, volutpat suscipit nulla. Pellentesque pharetra vulputate erat ut lobortis. Vivamus sed nibh enim. Phasellus pharetra malesuada justo quis molestie. Sed eget lectus at turpis elementum posuere quis ut velit. Morbi suscipit venenatis tempus. Ut sed arcu ipsum.")
|
||||
//
|
||||
// Text(
|
||||
// "Ut vulputate ultrices erat, sit amet auctor felis tristique vel. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Morbi felis leo, blandit cursus tortor eu, ullamcorper volutpat orci. Donec ornare velit sed neque scelerisque, sit amet malesuada nunc laoreet. Ut non lobortis felis, vel ultrices ligula. Nam massa mauris, volutpat et felis lacinia"
|
||||
// + Image(systemName: "x.circle")
|
||||
// + ", dignissim efficitur arcu. Vestibulum euismod in risus at consequat. Nunc a vestibulum dolor. Vivamus sed augue lectus. Duis felis leo, gravida quis est ac, volutpat suscipit nulla. Pellentesque pharetra vulputate erat ut lobortis. Vivamus sed nibh enim. Phasellus pharetra malesuada justo quis molestie. Sed eget lectus at turpis elementum posuere quis ut velit. Morbi suscipit venenatis tempus. Ut sed arcu ipsum."
|
||||
// )
|
||||
//// let m: Image = img
|
||||
////// .resizable()
|
||||
////// .scaledToFill()
|
||||
////// .frame(width: 80, height: 80)
|
||||
////
|
||||
//// Text("TEST") +
|
||||
//// Text(m.frame(width: 80, height: 80)).font(.system(size: 100)) +
|
||||
//// Text("test2")
|
||||
// } placeholder: {
|
||||
// Text("Loading...")
|
||||
// }
|
||||
//
|
||||
//// HStack(alignment: .top, spacing: 10) {
|
||||
//// VStack(alignment: .leading, spacing: 10) {
|
||||
//// Image(systemName: "square.fill")
|
||||
//// .resizable()
|
||||
//// .frame(width: 80, height: 80)
|
||||
//// .foregroundColor(.black)
|
||||
////
|
||||
//// Text("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed fermentum vulputate euismod. In facilisis diam et mollis consequat jkahdfk ladshfkljahsd fjlkahsd fjlkhasd kjlhasdjk faljk dsfljah sdfjlk hasdklj asdnf msdnf m,.asnf asdjhf ljaskhf lkjasdh fljakshd fljkash. ")
|
||||
//// }
|
||||
//// .frame(maxWidth: 80, alignment: .topLeading)
|
||||
////
|
||||
//// Text("Ut vulputate ultrices erat, sit amet auctor felis tristique vel. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Morbi felis leo, blandit cursus tortor eu, ullamcorper volutpat orci. Donec ornare velit sed neque scelerisque, sit amet malesuada nunc laoreet. Ut non lobortis felis, vel ultrices ligula. Nam massa mauris, volutpat et felis lacinia, dignissim efficitur arcu. Vestibulum euismod in risus at consequat. Nunc a vestibulum dolor. Vivamus sed augue lectus. Duis felis leo, gravida quis est ac, volutpat suscipit nulla. Pellentesque pharetra vulputate erat ut lobortis. Vivamus sed nibh enim. Phasellus pharetra malesuada justo quis molestie. Sed eget lectus at turpis elementum posuere quis ut velit. Morbi suscipit venenatis tempus. Ut sed arcu ipsum. ")
|
||||
//// }
|
||||
// }
|
||||
// .padding()
|
||||
//
|
||||
//
|
||||
//// Text("Ut vulputate ultrices erat, sit amet auctor felis tristique vel. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Morbi felis leo, blandit cursus tortor eu, ullamcorper volutpat orci. Donec ornare velit sed neque scelerisque, sit amet malesuada nunc laoreet. Ut non lobortis felis, vel ultrices ligula. Nam massa mauris, volutpat et felis lacinia") +
|
||||
////// Text(img.frame(width: 80, height: 80)) +
|
||||
//// Text(Image(systemName: "x.circle")).font(.system(size: 20)) +
|
||||
//// Text(", dignissim efficitur arcu. Vestibulum euismod in risus at consequat. Nunc a vestibulum dolor. Vivamus sed augue lectus. Duis felis leo, gravida quis est ac, volutpat suscipit nulla. Pellentesque pharetra vulputate erat ut lobortis. Vivamus sed nibh enim. Phasellus pharetra malesuada justo quis molestie. Sed eget lectus at turpis elementum posuere quis ut velit. Morbi suscipit venenatis tempus. Ut sed arcu ipsum.")
|
||||
// }
|
||||
//}
|
||||
//
|
||||
////struct DynamicTextModifier: ViewModifier {
|
||||
//// let imageWidth: CGFloat
|
||||
//// let text: String
|
||||
//// let font: Font
|
||||
////
|
||||
//// func body(content: Content) -> some View {
|
||||
//// GeometryReader { geometry in
|
||||
//// let lineWidth = geometry.size.width - imageWidth
|
||||
//// let textFragments = splitTextToFitWidth(text: text, lineWidth: lineWidth, font: font)
|
||||
////
|
||||
//// VStack(alignment: .leading, spacing: 0) {
|
||||
//// ForEach(textFragments.indices, id: \.self) { index in
|
||||
//// if index == 0 {
|
||||
//// HStack {
|
||||
//// Spacer(minLength: imageWidth)
|
||||
//// Text(textFragments[index]).font(font)
|
||||
//// }
|
||||
//// } else {
|
||||
//// Text(textFragments[index]).font(font)
|
||||
//// }
|
||||
//// }
|
||||
//// }
|
||||
//// }
|
||||
//// }
|
||||
////
|
||||
//// private func splitTextToFitWidth(text: String, lineWidth: CGFloat, font: Font) -> [String] {
|
||||
//// let words = text.split(separator: " ")
|
||||
//// var textFragments: [String] = []
|
||||
//// var currentLine = ""
|
||||
////
|
||||
//// for word in words {
|
||||
//// let newLine = currentLine.isEmpty ? "\(word)" : "\(currentLine) \(word)"
|
||||
//// let newSize = newLine.size(usingFont: font)
|
||||
////
|
||||
//// if newSize.width <= lineWidth {
|
||||
//// currentLine = newLine
|
||||
//// } else {
|
||||
//// textFragments.append(currentLine)
|
||||
//// currentLine = "\(word)"
|
||||
//// }
|
||||
//// }
|
||||
////
|
||||
//// if !currentLine.isEmpty {
|
||||
//// textFragments.append(currentLine)
|
||||
//// }
|
||||
////
|
||||
//// return textFragments
|
||||
//// }
|
||||
////}
|
||||
////
|
||||
////extension String {
|
||||
//// func size(usingFont font: Font) -> CGSize {
|
||||
//// let nsString = self as NSString
|
||||
//// let fontAttributes = [NSAttributedString.Key.font: UIFont.systemFont(ofSize: font.size)] // Adjust this line if you use a custom font
|
||||
//// return nsString.size(withAttributes: fontAttributes)
|
||||
//// }
|
||||
////}
|
||||
////
|
||||
////extension Font {
|
||||
//// var size: CGFloat {
|
||||
//// switch self {
|
||||
//// case .largeTitle: return UIFont.preferredFont(forTextStyle: .largeTitle).pointSize
|
||||
//// case .title: return UIFont.preferredFont(forTextStyle: .title1).pointSize
|
||||
//// case .title2: return UIFont.preferredFont(forTextStyle: .title2).pointSize
|
||||
//// case .title3: return UIFont.preferredFont(forTextStyle: .title3).pointSize
|
||||
//// case .headline: return UIFont.preferredFont(forTextStyle: .headline).pointSize
|
||||
//// case .subheadline: return UIFont.preferredFont(forTextStyle: .subheadline).pointSize
|
||||
//// case .body: return UIFont.preferredFont(forTextStyle: .body).pointSize
|
||||
//// case .callout: return UIFont.preferredFont(forTextStyle: .callout).pointSize
|
||||
//// case .caption: return UIFont.preferredFont(forTextStyle: .caption1).pointSize
|
||||
//// case .caption2: return UIFont.preferredFont(forTextStyle: .caption2).pointSize
|
||||
//// case .footnote: return UIFont.preferredFont(forTextStyle: .footnote).pointSize
|
||||
//// default: return UIFont.preferredFont(forTextStyle: .body).pointSize
|
||||
//// }
|
||||
//// }
|
||||
////}
|
||||
////
|
||||
////import SwiftUI
|
||||
////
|
||||
////struct Demo11: View {
|
||||
//// @State private var dynamicText: String = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut dapibus malesuada turpis, non posuere est laoreet nec. Sed maximus bibendum odio, a rhoncus nisl facilisis a. Curabitur vitae consequat justo."
|
||||
////
|
||||
//// var body: some View {
|
||||
//// ZStack(alignment: .topLeading) {
|
||||
//// Color.clear.modifier(DynamicTextModifier(imageWidth: 80, text: dynamicText, font: .body))
|
||||
////
|
||||
//// Image(systemName: "square.fill") // Replace with your own image
|
||||
//// .resizable()
|
||||
//// .frame(width: 100, height: 100) // Adjust the frame size to match your image size
|
||||
//// .background(Color.red) // Optional, just to visualize the image frame
|
||||
//// }
|
||||
//// .padding()
|
||||
//// }
|
||||
////}
|
||||
////
|
||||
//@available(iOS 16.0, *)
|
||||
//struct Demo11_Previews: PreviewProvider {
|
||||
// static var previews: some View {
|
||||
// Demo11()
|
||||
// }
|
||||
//}
|
|
@ -0,0 +1 @@
|
|||
**NOTE:** Ignore this directory. Nothing here is important, this directory will be deleted soon.
|
|
@ -1,10 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "group:FutureFlow">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:FutureFlowDemo/FutureFlowDemo.xcodeproj">
|
||||
</FileRef>
|
||||
</Workspace>
|
|
@ -1,33 +0,0 @@
|
|||
// swift-tools-version: 5.8
|
||||
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
||||
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "FutureFlow",
|
||||
platforms: [
|
||||
.iOS(.v14),
|
||||
.macOS(.v11),
|
||||
.tvOS(.v14),
|
||||
.watchOS(.v7)
|
||||
],
|
||||
products: [
|
||||
.library(
|
||||
name: "FutureFlow",
|
||||
targets: ["FutureFlow"]
|
||||
),
|
||||
],
|
||||
targets: [
|
||||
.target(
|
||||
name: "FutureFlow",
|
||||
dependencies: [],
|
||||
path: "Sources"
|
||||
),
|
||||
.testTarget(
|
||||
name: "FutureFlowTests",
|
||||
dependencies: ["FutureFlow"],
|
||||
path: "Tests/FutureFlowTests"
|
||||
),
|
||||
],
|
||||
swiftLanguageVersions: [.v5]
|
||||
)
|
|
@ -1,12 +0,0 @@
|
|||
//
|
||||
// FutureFlowManager.swift
|
||||
//
|
||||
//
|
||||
// Created by Muhand Jumah on 4/23/23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
internal class FutureFlowManager {
|
||||
internal static var currentNamespaces: [String:Namespace.ID] = [:]
|
||||
}
|
|
@ -1,179 +0,0 @@
|
|||
//
|
||||
// HighlightingView.swift
|
||||
//
|
||||
//
|
||||
// Created by Muhand Jumah on 4/23/23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
internal struct HighlightingView<Content: View, Chunk: FlowChunk>: View {
|
||||
// Binding variables
|
||||
@Binding var showTutorial: Bool
|
||||
|
||||
// Private variables
|
||||
let namespace: Namespace.ID
|
||||
let chunks: [Chunk]
|
||||
let content: Content
|
||||
|
||||
#if DEBUG
|
||||
// State variables (TODO: Delete)
|
||||
@State private var currentIndex: Int = 0
|
||||
#endif
|
||||
|
||||
@Namespace private var namespace2
|
||||
|
||||
internal init(namespace: Namespace.ID, showTutorial: Binding<Bool>, chunks: [Chunk], @ViewBuilder content: () -> Content) {
|
||||
self.namespace = namespace
|
||||
self._showTutorial = showTutorial
|
||||
self.chunks = chunks
|
||||
self.content = content()
|
||||
}
|
||||
|
||||
internal var body: some View {
|
||||
ZStack {
|
||||
self.content
|
||||
|
||||
SpotlightView(
|
||||
chunk: self.chunks[self.currentIndex],
|
||||
namespace: self.namespace,
|
||||
namespace2: self.namespace2
|
||||
)
|
||||
.opacity(self.showTutorial ? 1 : 0)
|
||||
.animation(
|
||||
.easeOut,
|
||||
value: self.showTutorial ? self.currentIndex : nil
|
||||
)
|
||||
.overlay(
|
||||
GeometryReader { reader in
|
||||
instructionsOverlay(reader: reader)
|
||||
.animation(.easeOut, value: self.showTutorial ? self.currentIndex : nil)
|
||||
// Text("\(reader.size.width) X \(reader.size.height)")
|
||||
// ZStack {
|
||||
// Color.blue
|
||||
// }
|
||||
}
|
||||
.matchedGeometryEffect(id: 10000, in: self.namespace2, isSource: false)
|
||||
// .overlay(
|
||||
// GeometryReader { reader in
|
||||
// Text("\(reader.size.width) X \(reader.size.height)")
|
||||
// }
|
||||
// )
|
||||
)
|
||||
|
||||
|
||||
//#if DEBUG
|
||||
// Button(action: {
|
||||
// if self.currentIndex < self.chunks.count - 1{
|
||||
// self.currentIndex += 1
|
||||
// } else {
|
||||
// self.currentIndex = 0
|
||||
// }
|
||||
// }) {
|
||||
// Text("NEXT")
|
||||
// }
|
||||
//#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension HighlightingView {
|
||||
func advance() {
|
||||
if self.currentIndex < self.chunks.count - 1{
|
||||
self.currentIndex += 1
|
||||
} else {
|
||||
self.currentIndex = 0
|
||||
}
|
||||
}
|
||||
|
||||
func previous() {
|
||||
if self.currentIndex > 0 {
|
||||
self.currentIndex -= 1
|
||||
} else {
|
||||
self.currentIndex = self.chunks.count - 1
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder func instructionsOverlay(reader: GeometryProxy) -> some View {
|
||||
if let instructionsViewType = self.chunks[self.currentIndex].instructionsViewType {
|
||||
let v = instructionsViewType
|
||||
.body(
|
||||
currentIndex: self.$currentIndex,
|
||||
maxCount: self.chunks.count,
|
||||
advance: self.advance,
|
||||
previous: self.previous
|
||||
)
|
||||
ZStack {
|
||||
// TODO: Find a better way than this hacky method
|
||||
v
|
||||
.opacity(0)
|
||||
}
|
||||
.overlay(
|
||||
GeometryReader { rr in
|
||||
v
|
||||
.frame(width: UIScreen.main.bounds.size.width)
|
||||
.offset(
|
||||
x: -(UIScreen.main.bounds.size.width - reader.size.width) / 2,
|
||||
y: self.getYOffset(position: v.position, in: v.position == .below ? reader.size : rr.size)
|
||||
)
|
||||
}
|
||||
|
||||
)
|
||||
// v
|
||||
// .overlay(
|
||||
// Text("\(reader.size.height)").allowsHitTesting(false))
|
||||
|
||||
// .offset(
|
||||
// x: -(UIScreen.main.bounds.size.width - reader.size.width) / 2,
|
||||
// y: self.getYOffset(position: v.position, in: reader.size)
|
||||
// )
|
||||
|
||||
// y: self.getYOffset(position: v.position, in: reader.size)
|
||||
// )
|
||||
}
|
||||
// ZStack {
|
||||
|
||||
// if let sle
|
||||
|
||||
// if(self.chunks[self.currentIndex].instruction != nil) {
|
||||
// SimpleInstructionsBubble(text: "TEST")
|
||||
// .frame(width: 200, height: 100, alignment: .center)
|
||||
// self.chunks[self.currentIndex].instruction!.bubble.body(instruction: self.chunks[self.currentIndex].instruction!)
|
||||
// }
|
||||
// .offset(x: 0, y: reader.size.height + 20)
|
||||
|
||||
// if let instruction = self.chunks[self.currentIndex].instruction {
|
||||
// self
|
||||
// .chunks[self.currentIndex]
|
||||
// .instructionsViewType!
|
||||
//// .inWithEnum()
|
||||
// .body(adv: self.advance, prev: self.previous)
|
||||
// .instructionsView(advance: self.advance, previous: self.previous)
|
||||
// instruction.bubble.body(instruction: instruction)
|
||||
// .padding(.horizontal, 15)
|
||||
// .frame(width: UIScreen.main.bounds.size.width)
|
||||
//// .offset(x: 0, y: 400)
|
||||
// .offset(
|
||||
// x: -(UIScreen.main.bounds.size.width - reader.size.width) / 2,
|
||||
// y: self.getYOffset(position: instruction.position, in: reader.size)
|
||||
// )
|
||||
// }
|
||||
|
||||
// if (self.chunk.instructionsBubble != nil) {
|
||||
// self.chunk.instructionsBubble!.body
|
||||
// .padding(.horizontal)
|
||||
// .frame(width: UIScreen.main.bounds.size.width)
|
||||
// .offset(x: 0, y: self.getYOffset(position: self.chunk.instructionsBubble!.position, in: reader.size))
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
func getYOffset(position: InstructionsViewPosition, in frame: CGSize) -> CGFloat {
|
||||
switch position {
|
||||
case .above:
|
||||
return -(frame.height + 20)
|
||||
default:
|
||||
return (frame.height + 20)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
//
|
||||
// InstructionsView.swift
|
||||
//
|
||||
//
|
||||
// Created by Muhand Jumah on 4/23/23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
public struct InstructionsView : View {
|
||||
internal let position: InstructionsViewPosition
|
||||
private let content: AnyView
|
||||
|
||||
internal init<Content: View>
|
||||
(
|
||||
position: InstructionsViewPosition = .below,
|
||||
@ViewBuilder _ content: () -> Content
|
||||
) {
|
||||
self.position = position
|
||||
self.content = content().eraseToAnyView()
|
||||
}
|
||||
|
||||
@_spi(Private)
|
||||
public var body: some View {
|
||||
self.content
|
||||
}
|
||||
}
|
|
@ -1,136 +0,0 @@
|
|||
//
|
||||
// InstructionsViewType.swift
|
||||
//
|
||||
//
|
||||
// Created by Muhand Jumah on 4/26/23.
|
||||
//
|
||||
|
||||
public enum InstructionsViewType {
|
||||
case simple(instructions: [String], position: InstructionsViewPosition = .below)
|
||||
case custom(view: (_ advance: @escaping () -> (), _ previous: @escaping () -> ()) -> InstructionsView)
|
||||
|
||||
internal func body(
|
||||
currentIndex: Binding<Int>,
|
||||
maxCount: Int,
|
||||
advance: @escaping () -> (),
|
||||
previous: @escaping () -> ()
|
||||
) -> InstructionsView {
|
||||
switch self {
|
||||
case .simple(let instructions, let position):
|
||||
return SimpleInstructionsViewWrapper(currentGlobalIndex: currentIndex,
|
||||
globalMaxCount: maxCount,
|
||||
advance: advance,
|
||||
previous: previous,
|
||||
instructions: instructions)
|
||||
.convertToInstructionsView(position: position)
|
||||
case .custom(let view):
|
||||
return view(advance, previous)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Rework this piece
|
||||
import SwiftUI
|
||||
|
||||
internal struct SimpleInstructionsViewWrapper: View {
|
||||
// Global instructions
|
||||
@Binding var currentGlobalIndex: Int
|
||||
var globalMaxCount: Int
|
||||
let advance: () -> ()
|
||||
let previous: () -> ()
|
||||
|
||||
private(set) var instructions: [String]
|
||||
@State private var currentInstructionIndex: Int = 0
|
||||
@State private var showPrevious: Bool = false
|
||||
@State private var showNext: Bool = false
|
||||
|
||||
var body: some View {
|
||||
SimpleInstructionsView(
|
||||
text: self.instructions[self.currentInstructionIndex],
|
||||
showPrevious: .init (
|
||||
get: {
|
||||
return (self.currentGlobalIndex > 0 && self.globalMaxCount > 0) || (self.currentInstructionIndex > 0 && self.instructions.count > 0)
|
||||
},
|
||||
set: { _ in}
|
||||
),
|
||||
showNext: .init(
|
||||
get: {
|
||||
return self.currentInstructionIndex < instructions.count - 1 || self.currentGlobalIndex < self.globalMaxCount - 1
|
||||
},
|
||||
set: { _ in }
|
||||
)
|
||||
)
|
||||
.onTapNext {
|
||||
guard currentInstructionIndex == instructions.count - 1 else {
|
||||
currentInstructionIndex += 1
|
||||
return
|
||||
}
|
||||
|
||||
advance()
|
||||
self.currentInstructionIndex = 0
|
||||
}
|
||||
.onTapPrev {
|
||||
guard currentInstructionIndex == 0 else {
|
||||
currentInstructionIndex -= 1
|
||||
return
|
||||
}
|
||||
// guard self.currentInstructionIndex > 0 else {
|
||||
// return
|
||||
// }
|
||||
// self.currentInstructionIndex -= 1
|
||||
self.currentInstructionIndex = 0
|
||||
previous()
|
||||
// self.currentInstructionIndex = instructions.count
|
||||
|
||||
}
|
||||
.overlay(
|
||||
VStack {
|
||||
Text("\(self.globalMaxCount)")
|
||||
Text("\(self.currentGlobalIndex)")
|
||||
Text("\(self.instructions.count)")
|
||||
Text("\(self.currentInstructionIndex)")
|
||||
}
|
||||
.offset(x: 0, y: 100)
|
||||
)
|
||||
.padding(.horizontal, 15)
|
||||
// .onChange(of: self.currentInstructionIndex, perform: { newValue in
|
||||
// // For the next button
|
||||
// if(newValue < instructions.count - 1 || currentGlobalIndex < globalMaxCount - 1) {
|
||||
// self.showNext = true
|
||||
// } else {
|
||||
// self.showNext = false
|
||||
// }
|
||||
//
|
||||
// if(newValue > 0 && self.instructions.count > 0) {
|
||||
// self.showPrevious = true
|
||||
// } else {
|
||||
// self.showPrevious = false
|
||||
// }
|
||||
// })
|
||||
.onAppear {
|
||||
// Next
|
||||
if(currentInstructionIndex < instructions.count - 1 || currentGlobalIndex < globalMaxCount) {
|
||||
self.showNext = true
|
||||
} else {
|
||||
self.showNext = false
|
||||
}
|
||||
// if(currentInstructionIndex < instructions.count - 1) {
|
||||
// self.showNext = true
|
||||
// } else {
|
||||
// self.showNext = false
|
||||
// }
|
||||
//
|
||||
if(currentInstructionIndex > 0 && self.instructions.count > 0) {
|
||||
self.showPrevious = true
|
||||
} else {
|
||||
self.showPrevious = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//struct SimpleInstructionsViewWrapper_Previews: PreviewProvider {
|
||||
// static var previews: some View {
|
||||
// SimpleInstructionsViewWrapper(instructions: ["Hello", "World", "askdlfjalk shfjlka dhfkj ahsdfkhdj"])
|
||||
// }
|
||||
//}
|
|
@ -1,146 +0,0 @@
|
|||
//
|
||||
// SimpleInstructionsView.swift
|
||||
//
|
||||
//
|
||||
// Created by Muhand Jumah on 4/23/23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct SimpleInstructionsView: View {
|
||||
private var text: String
|
||||
private var nextTapped: () -> ()
|
||||
private var prevTapped: () -> ()
|
||||
|
||||
@Binding var showPrevious: Bool
|
||||
@Binding var showNext: Bool
|
||||
|
||||
public init(
|
||||
text: String,
|
||||
showPrevious: Binding<Bool> = .constant(false),
|
||||
showNext: Binding<Bool> = .constant(false)
|
||||
) {
|
||||
self.text = text
|
||||
self.nextTapped = { }
|
||||
self.prevTapped = { }
|
||||
self._showPrevious = showPrevious
|
||||
self._showNext = showNext
|
||||
}
|
||||
|
||||
fileprivate init(
|
||||
text: String,
|
||||
nextTapped: @escaping () -> (),
|
||||
prevTapped: @escaping () -> (),
|
||||
showPrevious: Binding<Bool> = .constant(false),
|
||||
showNext: Binding<Bool> = .constant(false)
|
||||
) {
|
||||
self.text = text
|
||||
self.nextTapped = nextTapped
|
||||
self.prevTapped = prevTapped
|
||||
self._showPrevious = showPrevious
|
||||
self._showNext = showNext
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
// Color.purple
|
||||
VStack(alignment:.leading, spacing: 20) {
|
||||
HStack(alignment: .top, spacing: 10) {
|
||||
Image(systemName: "lightbulb.circle.fill")
|
||||
.font(.system(size: 32))
|
||||
.foregroundColor(.blue)
|
||||
|
||||
Text(self.text)
|
||||
.font(.body)
|
||||
}
|
||||
|
||||
if (self.showPrevious || self.showNext) {
|
||||
HStack(alignment: .center, spacing:15) {
|
||||
Spacer()
|
||||
if(self.showPrevious) {
|
||||
Button(action: {
|
||||
self.prevTapped()
|
||||
}) {
|
||||
Text("Previous")
|
||||
.font(.callout)
|
||||
.fontWeight(.medium)
|
||||
.foregroundColor(.blue)
|
||||
}
|
||||
}
|
||||
|
||||
if(self.showNext) {
|
||||
|
||||
Button(action: {
|
||||
self.nextTapped()
|
||||
}) {
|
||||
Text("Next")
|
||||
.font(.callout)
|
||||
.fontWeight(.semibold)
|
||||
.padding(.horizontal, 10)
|
||||
.padding(.vertical, 5)
|
||||
.background(Color.blue)
|
||||
.cornerRadius(3)
|
||||
.foregroundColor(.white)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
.padding(.vertical, 15)
|
||||
.background(Color.white)
|
||||
.cornerRadius(10)
|
||||
.shadow(color: .black.opacity(0.2), radius: 5, x: 0, y: 0)
|
||||
}
|
||||
|
||||
func onTapNext(_ callback: @escaping () -> ()) -> Self {
|
||||
return .init(
|
||||
text: self.text,
|
||||
nextTapped: callback,
|
||||
prevTapped: self.prevTapped,
|
||||
showPrevious: self.$showPrevious,
|
||||
showNext: self.$showNext
|
||||
)
|
||||
}
|
||||
|
||||
func onTapPrev(_ callback: @escaping () -> ()) -> Self {
|
||||
return .init(
|
||||
text: self.text,
|
||||
nextTapped: self.nextTapped,
|
||||
prevTapped: callback,
|
||||
showPrevious: self.$showPrevious,
|
||||
showNext: self.$showNext
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
struct SimpleInstructionsView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ZStack {
|
||||
Color.white
|
||||
.edgesIgnoringSafeArea(.all)
|
||||
|
||||
VStack(spacing: 5) {
|
||||
SimpleInstructionsView(text: "This is just a tutorial asjdkfhla sdfhlakjsd hflkajsd hflkjasdhf lkjasdhf kljasd lajsdfh lkjsdhf lakjsdhf akjsl hfakjsd hadffajsdkfhlkajs hflkjas hfklja hsdjkfl haljskd fhljasdfhlaksd hflas dhfklj asdh fljasdlfjsdhfl.")
|
||||
|
||||
SimpleInstructionsView(text: "This is just a tutorial asjdkfhla sdfhlakjsd hflkajsd hflkjasdhf lkjasdhf kljasd lajsdfh lkjsdhf lakjsdhf akjsl hfakjsd hadffajsdkfhlkajs hflkjas hfklja hsdjkfl haljskd fhljasdfhlaksd hflas dhfklj asdh fljasdlfjsdhfl.", showNext: .constant(true))
|
||||
.onTapNext {
|
||||
print("Next Tapped")
|
||||
}
|
||||
|
||||
SimpleInstructionsView(text: "This is just a tutorial asjdkfhla sdfhlakjsd hflkajsd hflkjasdhf lkjasdhf kljasd lajsdfh lkjsdhf lakjsdhf akjsl hfakjsd hadffajsdkfhlkajs hflkjas hfklja hsdjkfl haljskd fhljasdfhlaksd hflas dhfklj asdh fljasdlfjsdhfl.", showPrevious: .constant(true))
|
||||
.onTapPrev {
|
||||
print("Previous Tapped")
|
||||
}
|
||||
|
||||
SimpleInstructionsView(text: "This is just a tutorial asjdkfhla sdfhlakjsd hflkajsd hflkjasdhf lkjasdhf kljasd lajsdfh lkjsdhf lakjsdhf akjsl hfakjsd hadffajsdkfhlkajs hflkjas hfklja hsdjkfl haljskd fhljasdfhlaksd hflas dhfklj asdh fljasdlfjsdhfl.", showPrevious: .constant(true), showNext: .constant(true))
|
||||
.onTapNext {
|
||||
print("Next Tapped")
|
||||
}
|
||||
.onTapPrev {
|
||||
print("Previous Tapped")
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,70 +0,0 @@
|
|||
//
|
||||
// SpotlightView.swift
|
||||
//
|
||||
//
|
||||
// Created by Muhand Jumah on 4/23/23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
internal struct SpotlightView<Chunk: FlowChunk>: View {
|
||||
private var chunk: Chunk
|
||||
private var namespace: Namespace.ID
|
||||
private var namespace2: Namespace.ID
|
||||
|
||||
internal init(chunk: Chunk, namespace: Namespace.ID, namespace2: Namespace.ID) {
|
||||
self.chunk = chunk
|
||||
self.namespace = namespace
|
||||
self.namespace2 = namespace2
|
||||
}
|
||||
|
||||
internal var body: some View {
|
||||
ZStack {
|
||||
self.chunk.spotlightBackground.body(self.namespace)
|
||||
|
||||
// GeometryReader { reader in
|
||||
self.chunk.spotlightShape.body(self.namespace)
|
||||
.blendMode(.destinationOut)
|
||||
.matchedGeometryEffect(id: 10000, in: self.namespace2, isSource: true)
|
||||
// .overlay(
|
||||
// instructionsOverlay(reader: reader),
|
||||
// alignment: .top
|
||||
// )
|
||||
// }
|
||||
.matchedGeometryEffect(
|
||||
id: self.chunk.id,
|
||||
in: namespace,
|
||||
properties: .frame, anchor: .center,
|
||||
isSource: false
|
||||
)
|
||||
// .blendMode(.destinationOut)
|
||||
}
|
||||
.compositingGroup()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Helper Methods
|
||||
//private extension SpotlightView {
|
||||
// func instructionsOverlay(reader: GeometryProxy) -> some View {
|
||||
// ZStack {
|
||||
// if(self.chunk.instructions.count > 0) {
|
||||
// Text("TEST")
|
||||
// }
|
||||
//// if (self.chunk.instructionsBubble != nil) {
|
||||
//// self.chunk.instructionsBubble!.body
|
||||
//// .padding(.horizontal)
|
||||
//// .frame(width: UIScreen.main.bounds.size.width)
|
||||
//// .offset(x: 0, y: self.getYOffset(position: self.chunk.instructionsBubble!.position, in: reader.size))
|
||||
//// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// func getYOffset(position: InstructionsBubblePosition, in frame: CGSize) -> CGFloat {
|
||||
// switch position {
|
||||
// case .above:
|
||||
// return -(frame.height + 20)
|
||||
// default:
|
||||
// return (frame.height + 20)
|
||||
// }
|
||||
// }
|
||||
//}
|
|
@ -1,8 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>IDEDidComputeMac32BitWarning</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
|
@ -1,91 +0,0 @@
|
|||
//
|
||||
// ContentView.swift
|
||||
// FutureFlowDemo
|
||||
//
|
||||
// Created by Muhand Jumah on 4/21/23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import FutureFlow
|
||||
|
||||
|
||||
enum TutorialChunks: FlowChunk, CaseIterable {
|
||||
case red
|
||||
case green
|
||||
case yellow
|
||||
|
||||
var spotlightShape: SpotlightShape {
|
||||
switch self {
|
||||
case .yellow:
|
||||
return .circle()
|
||||
default:
|
||||
return .rectangle()
|
||||
}
|
||||
}
|
||||
|
||||
var spotlightBackground: SpotlightBackground {
|
||||
switch self {
|
||||
case .green:
|
||||
return .ultraThinMaterial
|
||||
default:
|
||||
return .black
|
||||
}
|
||||
}
|
||||
|
||||
var instructionsViewType: InstructionsViewType? {
|
||||
switch self {
|
||||
case .red:
|
||||
return .simple(instructions: ["Text1"], position: .below)
|
||||
case .green:
|
||||
return .simple(instructions: ["Text2", "Text22", "Text222", "Text2222"], position: .below)
|
||||
case .yellow:
|
||||
return .simple(instructions: ["Text3"], position: .above)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ContentView: View {
|
||||
private var uniqueIdentifier: String = UUID().uuidString
|
||||
private var tutorial: [TutorialChunks] =
|
||||
[
|
||||
.red,
|
||||
.green,
|
||||
.yellow
|
||||
]
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 50) {
|
||||
Rectangle()
|
||||
.frame(width: 256, height: 256, alignment: .center)
|
||||
.foregroundColor(Color.red)
|
||||
.configureChunkForSpotlight(
|
||||
parentIdentifier: self.uniqueIdentifier,
|
||||
chunk: self.tutorial[0]
|
||||
)
|
||||
|
||||
Rectangle()
|
||||
.frame(width: 64, height: 128, alignment: .center)
|
||||
.foregroundColor(Color.green)
|
||||
.configureChunkForSpotlight(
|
||||
parentIdentifier: self.uniqueIdentifier,
|
||||
chunk: self.tutorial[1]
|
||||
)
|
||||
|
||||
Circle()
|
||||
.frame(width: 128, height: 128, alignment: .center)
|
||||
.foregroundColor(.yellow)
|
||||
.configureChunkForSpotlight(
|
||||
parentIdentifier: self.uniqueIdentifier,
|
||||
chunk: self.tutorial[2]
|
||||
)
|
||||
}
|
||||
.padding()
|
||||
.assembleSpotlightChunks(uniqueIdentifier: self.uniqueIdentifier, chunks: Array(TutorialChunks.allCases))
|
||||
}
|
||||
}
|
||||
|
||||
struct ContentView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ContentView()
|
||||
}
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
////
|
||||
//// Demo11.swift
|
||||
//// FutureFlowDemo
|
||||
////
|
||||
//// Created by Muhand Jumah on 4/23/23.
|
||||
////
|
||||
//
|
||||
//import SwiftUI
|
||||
//
|
||||
//struct Demo11: View {
|
||||
//
|
||||
//}
|
||||
//
|
||||
//struct Demo11_Previews: PreviewProvider {
|
||||
// static var previews: some View {
|
||||
//
|
||||
//
|
||||
// }
|
||||
//}
|
|
@ -21,12 +21,13 @@ let package = Package(
|
|||
.target(
|
||||
name: "FutureFlow",
|
||||
dependencies: [],
|
||||
path: "FutureFlow/Sources"
|
||||
path: "Sources",
|
||||
exclude: ["./Examples"]
|
||||
),
|
||||
.testTarget(
|
||||
name: "FutureFlowTests",
|
||||
dependencies: ["FutureFlow"],
|
||||
path: "FutureFlow/Tests/FutureFlowTests"
|
||||
path: "Tests/FutureFlowTests"
|
||||
),
|
||||
],
|
||||
swiftLanguageVersions: [.v5]
|
||||
|
|
213
README.md
213
README.md
|
@ -9,3 +9,216 @@ As of **April 22nd, 2023**, there are no plans to support UIKit nor support any
|
|||
|
||||
If you however, would like to tackle backward compatibility or UIKit support, feel free to fork this repo and work on it and
|
||||
hit me up to merge your contribution and credit you.
|
||||
|
||||
## Installation
|
||||
### Swift Package Manager (SPM)
|
||||
To add FutureFlow to your project, use Xcode’s built-in support for Swift packages. Click File → Swift Packages → Add Package Dependency, and paste the following URL into the search field:
|
||||
|
||||
```
|
||||
https://github.com/xyfuture-llc/FutureFlow.git
|
||||
```
|
||||
|
||||
You’re then prompted to select the version to install and indicate the desired update policy. I recommend starting with the latest version (it’s selected automatically), and choosing “up to next major” as the preferred update rule. Once you click Next, the package is fetched. Then select the target you’re going to use FutureFlow in. Click Finish, and you’re ready to go.
|
||||
|
||||
### CocoaPods
|
||||
To be released
|
||||
|
||||
|
||||
## Usage
|
||||

|
||||
|
||||
A full example can be found inside the `Examples` directory.
|
||||
|
||||
In order to use FutureFlow you first need to create an `enum` that conforms to `FutureFlow` protocol (Conforming to `CaseIterable` is optional but it makes your life easy).
|
||||
|
||||
### Step 1
|
||||
```Swift
|
||||
enum TutorialChunks: FlowChunk, CaseIterable {
|
||||
}
|
||||
```
|
||||
|
||||
Inside the enum define the steps in which you want the spotlight to go to.
|
||||
|
||||
```Swift
|
||||
enum TutorialChunks: FlowChunk, CaseIterable {
|
||||
case red
|
||||
case green
|
||||
case yellow
|
||||
}
|
||||
```
|
||||
|
||||
This is as simple as it gets; However you can customize it further by defining the spotlight shape, the type of background, or even include an instructions view (A view where you instruct the user why are you shining the spot light here).
|
||||
|
||||
Here are the defined protocols
|
||||
```Swift
|
||||
var spotlightShape: SpotlightShape { get }
|
||||
var spotlightBackground: SpotlightBackground { get }
|
||||
|
||||
@InstructionsViewBuilder func instructionsView(
|
||||
_ next: @escaping () -> (),
|
||||
_ back: @escaping () -> ()
|
||||
) -> AnyInstructionsView?
|
||||
|
||||
var instructionsViewPosition: InstructionsViewPosition? { get }
|
||||
```
|
||||
|
||||
Here are the default values if the above is not defined:
|
||||
|
||||
- `spotlightShape`: `.rectangle`
|
||||
- `spotlightBackground`: `.black`
|
||||
- `instructionsView`: `nil`
|
||||
- `instructionsViewPosition`: `center`
|
||||
|
||||
You can customize a shape or a background by calling the `.custom` enum case such as
|
||||
|
||||
```Swift
|
||||
enum TutorialChunks: FlowChunk, CaseIterable {
|
||||
case red
|
||||
case green
|
||||
case yellow
|
||||
|
||||
var spotlightShape: SpotlightShape {
|
||||
return SpotlightShape.custom {
|
||||
Circle()
|
||||
.foregroundColor(.white)
|
||||
.blur(radius: 3)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Step 2
|
||||
After creating your enum you should create the view in which you want to shine spotlight on.
|
||||
|
||||
```Swift
|
||||
struct ContentView: View {
|
||||
var body: some View {
|
||||
VStack(spacing: 50) {
|
||||
Rectangle()
|
||||
.frame(width: 256, height: 256, alignment: .center)
|
||||
.foregroundColor(.red)
|
||||
|
||||
Rectangle()
|
||||
.frame(width: 64, height: 128, alignment: .center)
|
||||
.foregroundColor(Color.green)
|
||||
|
||||
Circle()
|
||||
.frame(width: 128, height: 128, alignment: .center)
|
||||
.foregroundColor(.yellow)
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Now you need to create a unique identifier for this view in which we will be using as well as a namespace and a variable to control if the spotlight view should be displayed or not (in this case I am calling it a "tutorial")
|
||||
|
||||
```Swift
|
||||
...
|
||||
private var uniqueIdentifier: String = UUID().uuidString
|
||||
|
||||
@State private var showTutorial: Bool = true
|
||||
@Namespace var namespace
|
||||
...
|
||||
```
|
||||
|
||||
As can be seen there must be a parent container to wrap the elements to shine a `spotlight` on. In this case I am using `VStack`.
|
||||
|
||||
Now you need to call `configureChunkForSpotlight` on each element you want to shine the spotlight on and you need to call `assembleSpotlightChunks` on the wrapper.
|
||||
|
||||
#### `configureChunkForSpotlight`
|
||||
Prepares the current view to be highlighted by the spotlight effect, associating it with the specified `FlowChunk`.
|
||||
|
||||
This function should be used to mark individual child views within a parent view that has the `assembleSpotlightChunks` function applied to it.
|
||||
|
||||
- Parameters:
|
||||
- namespace: The namespace that belongs to the view where the animations will be played.
|
||||
- parentIdentifier: A unique string identifier for the parent view containing the spotlight effect.
|
||||
- chunk: The `FlowChunk` associated with the current view, which determines the spotlight shape, background, and other properties.
|
||||
|
||||
- Returns: The modified view with the matched geometry effect applied, allowing it to be highlighted by the spotlight effect.
|
||||
|
||||
#### `assembleSpotlightChunks`
|
||||
Sets up the parent view to contain the spotlight effect, using the provided `FlowChunk` instances.
|
||||
|
||||
This function should be used to wrap the parent view containing child views that have the `configureChunkForSpotlight` function applied to them.
|
||||
|
||||
The chunks provided must be of the same `FlowChunk` type as the one used by the child views, to ensure that the spotlight effect works as intended.
|
||||
|
||||
- Parameters:
|
||||
- namespace: The namespace that belongs to the view where the animations will be played.
|
||||
- uniqueIdentifier: A unique string identifier for the parent view. This identifier must be the same as the one provided to the `configureChunkForSpotlight` function for child views.
|
||||
- chunks: An array of `FlowChunk` instances associated with the child views to be highlighted by the spotlight effect.
|
||||
- showTutorial: A boolean variable to toggle the spotlight view on and off.
|
||||
- onStepChange: An optional callback method that will be triggered when the current step is changed, basically when the spotlight shines on a different element than the current element.
|
||||
|
||||
- Returns: The modified view with the `HighlightingView` applied, containing the spotlight effect and its child views.
|
||||
|
||||
---
|
||||
|
||||
This is how the final view looks like
|
||||
|
||||
```Swift
|
||||
struct ContentView: View {
|
||||
private var uniqueIdentifier: String = UUID().uuidString
|
||||
|
||||
@State private var showTutorial: Bool = true
|
||||
@Namespace var namespace
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 50) {
|
||||
Rectangle()
|
||||
.frame(width: 256, height: 256, alignment: .center)
|
||||
.foregroundColor(.red)
|
||||
.configureChunkForSpotlight(
|
||||
namespace: self.namespace,
|
||||
parentIdentifier: self.uniqueIdentifier,
|
||||
chunk: TutorialChunks.red
|
||||
)
|
||||
|
||||
Rectangle()
|
||||
.frame(width: 64, height: 128, alignment: .center)
|
||||
.foregroundColor(Color.green)
|
||||
.configureChunkForSpotlight(
|
||||
namespace: self.namespace,
|
||||
parentIdentifier: self.uniqueIdentifier,
|
||||
chunk: TutorialChunks.green
|
||||
)
|
||||
|
||||
Circle()
|
||||
.frame(width: 128, height: 128, alignment: .center)
|
||||
.foregroundColor(.yellow)
|
||||
.configureChunkForSpotlight(
|
||||
namespace: self.namespace,
|
||||
parentIdentifier: self.uniqueIdentifier,
|
||||
chunk: TutorialChunks.yellow
|
||||
)
|
||||
.onTapGesture {
|
||||
self.showTutorial = true
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.assembleSpotlightChunks(namespace:self.namespace, uniqueIdentifier: self.uniqueIdentifier, chunks: Array(TutorialChunks.allCases), showTutorial: self.$showTutorial) {
|
||||
chunk in
|
||||
print("A new step")
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
That's it! This is the whole API.
|
||||
|
||||
## More
|
||||
You can browse teh source files to learn more about the other options available to you. You can also look at "Examples" directory as I will add more examples for different features/options.
|
||||
|
||||
## Contributing
|
||||
Contributions are welcome!
|
||||
|
||||
Have a look at [issues](https://github.com/xyfuture-llc/FutureFlow/issues) to see the project’s current needs. Don’t hesitate to create new issues, especially if you intend to work on them yourself.
|
||||
|
||||
If you’d like to discuss something else, contact me directly.
|
||||
|
||||
## License and Copyright
|
||||
FutureFlow is licensed under the MIT License. See the [LICENSE file](https://github.com/xyfuture-llc/FutureFlow/blob/main/LICENSE) for details.
|
||||
|
||||
© 2023–2023 xyFuture, LLC
|
||||
|
|
|
@ -14,12 +14,12 @@ public extension View {
|
|||
/// This function should be used to mark individual child views within a parent view that has the `assembleSpotlightChunks` function applied to it.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - namespace: The namespace that belongs to the view where the animations will be played.
|
||||
/// - parentIdentifier: A unique string identifier for the parent view containing the spotlight effect.
|
||||
/// - chunk: The `FlowChunk` associated with the current view, which determines the spotlight shape, background, and other properties.
|
||||
///
|
||||
/// - Returns: The modified view with the matched geometry effect applied, allowing it to be highlighted by the spotlight effect.
|
||||
func configureChunkForSpotlight<Chunk: FlowChunk>(parentIdentifier: String, chunk: Chunk) -> some View {
|
||||
let namespace = getNamespace(from: parentIdentifier)
|
||||
func configureChunkForSpotlight<Chunk: FlowChunk>(namespace: Namespace.ID, parentIdentifier: String, chunk: Chunk) -> some View {
|
||||
|
||||
return self
|
||||
.matchedGeometryEffect(
|
||||
|
@ -38,26 +38,26 @@ public extension View {
|
|||
/// The chunks provided must be of the same `FlowChunk` type as the one used by the child views, to ensure that the spotlight effect works as intended.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - namespace: The namespace that belongs to the view where the animations will be played.
|
||||
/// - uniqueIdentifier: A unique string identifier for the parent view. This identifier must be the same as the one provided to the `configureChunkForSpotlight` function for child views.
|
||||
/// - chunks: An array of `FlowChunk` instances associated with the child views to be highlighted by the spotlight effect.
|
||||
/// - showTutorial: A boolean variable to toggle the spotlight view on and off.
|
||||
/// - onStepChange: An optional callback method that will be triggered when the current step is changed, basically when the spotlight shines on a different element than the current element.
|
||||
///
|
||||
/// - Returns: The modified view with the `HighlightingView` applied, containing the spotlight effect and its child views.
|
||||
func assembleSpotlightChunks<Chunk: FlowChunk>(uniqueIdentifier: String, chunks: [Chunk]) -> some View {
|
||||
let namespace = getNamespace(from: uniqueIdentifier)
|
||||
func assembleSpotlightChunks<Chunk: FlowChunk>(namespace: Namespace.ID, uniqueIdentifier: String, chunks: [Chunk], showTutorial: Binding<Bool>, _ onStepChange: ((_ chunk: Chunk) -> ())? = nil) -> some View {
|
||||
|
||||
return HighlightingView(
|
||||
namespace: namespace,
|
||||
showTutorial: .constant(true),
|
||||
chunks: chunks
|
||||
) {
|
||||
self
|
||||
}
|
||||
}
|
||||
return (
|
||||
HighlightingView(
|
||||
namespace: namespace,
|
||||
showTutorial: showTutorial,
|
||||
chunks: chunks
|
||||
) {
|
||||
self
|
||||
}
|
||||
.onStepChange(onStepChange ?? { _ in })
|
||||
)
|
||||
|
||||
func convertToInstructionsView(position: InstructionsViewPosition) -> InstructionsView {
|
||||
InstructionsView(position: position) {
|
||||
self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -70,17 +70,3 @@ public extension View {
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Helper Methods
|
||||
fileprivate extension View {
|
||||
func getNamespace(from identifier: String) -> Namespace.ID {
|
||||
if let existingNamespace = FutureFlowManager.currentNamespaces[identifier] {
|
||||
return existingNamespace
|
||||
} else {
|
||||
let newNamespace = Namespace().wrappedValue
|
||||
FutureFlowManager.currentNamespaces[identifier] = newNamespace
|
||||
return newNamespace
|
||||
}
|
||||
}
|
||||
}
|
|
@ -11,7 +11,12 @@ public protocol FlowChunk: Hashable, Identifiable {
|
|||
var spotlightShape: SpotlightShape { get }
|
||||
var spotlightBackground: SpotlightBackground { get }
|
||||
|
||||
var instructionsViewType: InstructionsViewType? { get }
|
||||
@InstructionsViewBuilder func instructionsView(
|
||||
_ next: @escaping () -> (),
|
||||
_ back: @escaping () -> ()
|
||||
) -> AnyInstructionsView?
|
||||
|
||||
var instructionsViewPosition: InstructionsViewPosition? { get }
|
||||
}
|
||||
|
||||
public extension FlowChunk {
|
||||
|
@ -27,7 +32,14 @@ public extension FlowChunk {
|
|||
return .black
|
||||
}
|
||||
|
||||
var instructionsViewType: InstructionsViewType? {
|
||||
func instructionsView(
|
||||
_ next: @escaping () -> (),
|
||||
_ back: @escaping () -> ()
|
||||
) -> AnyInstructionsView? {
|
||||
nil
|
||||
}
|
||||
|
||||
var instructionsViewPosition: InstructionsViewPosition? {
|
||||
return nil
|
||||
}
|
||||
}
|
|
@ -0,0 +1,131 @@
|
|||
//
|
||||
// HighlightingView.swift
|
||||
//
|
||||
//
|
||||
// Created by Muhand Jumah on 4/23/23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
public final class Manager<Chunk: FlowChunk>: ObservableObject {
|
||||
@Published var totalChunks: [Chunk] = []
|
||||
@Published var currentStepIndex: Int = 0
|
||||
}
|
||||
|
||||
internal struct HighlightingView<Content: View, Chunk: FlowChunk>: View {
|
||||
@StateObject var manager: Manager<Chunk> = .init()
|
||||
|
||||
// Binding variables
|
||||
@Binding var showTutorial: Bool
|
||||
|
||||
// Private variables
|
||||
let namespace: Namespace.ID
|
||||
let chunks: [Chunk]
|
||||
let content: () -> Content
|
||||
|
||||
// #if DEBUG
|
||||
// State variables (TODO: Delete)
|
||||
@State private var currentIndex: Int = 0
|
||||
// #endif
|
||||
|
||||
@Namespace private var namespace2
|
||||
|
||||
private var onCurrentChunkChanged: ((_ newChunk: Chunk) -> ())?
|
||||
|
||||
internal init(
|
||||
namespace: Namespace.ID,
|
||||
showTutorial: Binding<Bool>,
|
||||
chunks: [Chunk],
|
||||
@ViewBuilder content: @escaping () -> Content)
|
||||
{
|
||||
self.namespace = namespace
|
||||
self._showTutorial = showTutorial
|
||||
self.chunks = chunks
|
||||
self.onCurrentChunkChanged = nil
|
||||
self.content = content
|
||||
}
|
||||
|
||||
fileprivate init(
|
||||
namespace: Namespace.ID,
|
||||
showTutorial: Binding<Bool>,
|
||||
chunks: [Chunk],
|
||||
onCurrentChunkChanged: @escaping (_ chunk: Chunk) -> (),
|
||||
content: @escaping () -> Content)
|
||||
{
|
||||
self.namespace = namespace
|
||||
self._showTutorial = showTutorial
|
||||
self.chunks = chunks
|
||||
self.onCurrentChunkChanged = onCurrentChunkChanged
|
||||
self.content = content
|
||||
}
|
||||
|
||||
internal var body: some View {
|
||||
ZStack {
|
||||
self.content()
|
||||
|
||||
ZStack {
|
||||
if (self.showTutorial) {
|
||||
let chunk = self.chunks[self.currentIndex]
|
||||
|
||||
SpotlightView(
|
||||
chunk: chunk,
|
||||
namespace: self.namespace,
|
||||
namespace2: self.namespace2
|
||||
)
|
||||
.opacity(self.showTutorial ? 1 : 0)
|
||||
.animation(.easeOut, value: self.showTutorial ? self.currentIndex : nil)
|
||||
|
||||
|
||||
chunk.instructionsView(self.advance, self.previous)
|
||||
.frame(width:UIScreen.main.bounds.width * 0.8)
|
||||
.offset(x:0, y:self.chunks[self.currentIndex].instructionsViewPosition == .below ? 20 : -20)
|
||||
.matchedGeometryEffect(id: 10001,
|
||||
in: self.namespace2,
|
||||
properties: .position,
|
||||
anchor: self.chunks[self.currentIndex].instructionsViewPosition == .below ? .top : .bottom,
|
||||
isSource: false)
|
||||
.animation(.easeOut, value: self.showTutorial ? self.currentIndex : nil)
|
||||
}
|
||||
}
|
||||
.transition(.opacity)
|
||||
.animation(.linear, value: self.showTutorial)
|
||||
}
|
||||
.onChange(of: self.showTutorial) { newValue in
|
||||
if(newValue == false) {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
|
||||
self.currentIndex = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
.onChange(of: self.currentIndex) { newValue in
|
||||
if let callback = self.onCurrentChunkChanged {
|
||||
callback(self.chunks[newValue])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func onStepChange(_ callback: @escaping (_ chunk: Chunk) -> ()) -> HighlightingView {
|
||||
.init(namespace: self.namespace,
|
||||
showTutorial: self.$showTutorial,
|
||||
chunks: self.chunks,
|
||||
onCurrentChunkChanged: callback,
|
||||
content: self.content)
|
||||
}
|
||||
}
|
||||
|
||||
private extension HighlightingView {
|
||||
func advance() {
|
||||
if self.currentIndex < self.chunks.count - 1{
|
||||
self.currentIndex += 1
|
||||
return
|
||||
}
|
||||
|
||||
self.showTutorial = false
|
||||
}
|
||||
|
||||
func previous() {
|
||||
if self.currentIndex > 0 {
|
||||
self.currentIndex -= 1
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
//
|
||||
// AnyInstructionsView.swift
|
||||
//
|
||||
//
|
||||
// Created by Muhand Jumah on 4/29/23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
public struct AnyInstructionsView: InstructionsView {
|
||||
private let _view: () -> AnyView
|
||||
|
||||
public var body: some View {
|
||||
_view()
|
||||
}
|
||||
|
||||
public init<I: InstructionsView>(_ instructionsView: I) {
|
||||
_view = {
|
||||
instructionsView
|
||||
.eraseToAnyView()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
//
|
||||
// InstructionsView.swift
|
||||
//
|
||||
//
|
||||
// Created by Muhand Jumah on 4/23/23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
public protocol InstructionsView: View {
|
||||
}
|
||||
|
||||
extension InstructionsView {
|
||||
func eraseToAnyInstructionsView() -> AnyInstructionsView {
|
||||
AnyInstructionsView(
|
||||
self
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
//
|
||||
// InstructionsViewBuilder.swift
|
||||
//
|
||||
//
|
||||
// Created by Muhand Jumah on 4/29/23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
@resultBuilder
|
||||
public struct InstructionsViewBuilder {
|
||||
public static func buildBlock<Content: InstructionsView>(_ instructionsView: Content?) -> AnyInstructionsView? {
|
||||
instructionsView?
|
||||
.eraseToAnyInstructionsView()
|
||||
}
|
||||
|
||||
public static func buildEither<Content: InstructionsView>(first component: Content?) -> AnyInstructionsView? {
|
||||
component?
|
||||
.eraseToAnyInstructionsView()
|
||||
}
|
||||
|
||||
public static func buildEither<Content: InstructionsView>(second component: Content?) -> AnyInstructionsView? {
|
||||
component?
|
||||
.eraseToAnyInstructionsView()
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,128 @@
|
|||
////
|
||||
//// InstructionsViewType.swift
|
||||
////
|
||||
////
|
||||
//// Created by Muhand Jumah on 4/26/23.
|
||||
////
|
||||
//
|
||||
//public enum InstructionsViewType {
|
||||
//// case simple(instructions: [String], position: InstructionsViewPosition = .below)
|
||||
//// case custom(view: (_ advance: @escaping () -> (), _ previous: @escaping () -> ()) -> InstructionsView)
|
||||
// case custom(view:(_ advance: @escaping () -> (), _ previous: @escaping () -> ()) -> AnyInstructionsView)
|
||||
////
|
||||
//// internal func body(
|
||||
//// currentIndex: Binding<Int>,
|
||||
//// maxCount: Int,
|
||||
//// advance: @escaping () -> (),
|
||||
//// previous: @escaping () -> ()
|
||||
//// ) -> InstructionsView {
|
||||
//// switch self {
|
||||
//// case .simple(let instructions, let position):
|
||||
//// return SimpleInstructionsViewWrapper(currentGlobalIndex: currentIndex,
|
||||
//// globalMaxCount: maxCount,
|
||||
//// advance: advance,
|
||||
//// previous: previous,
|
||||
//// instructions: instructions)
|
||||
//// .convertToInstructionsView(position: position)
|
||||
//// case .custom(let view):
|
||||
//// return view(advance, previous)
|
||||
//// }
|
||||
//// }
|
||||
//}
|
||||
//
|
||||
////// TODO: Rework this piece
|
||||
////import SwiftUI
|
||||
////
|
||||
////internal struct SimpleInstructionsViewWrapper: View {
|
||||
//// // Global instructions
|
||||
//// @Binding var currentGlobalIndex: Int
|
||||
//// var globalMaxCount: Int
|
||||
//// let advance: () -> ()
|
||||
//// let previous: () -> ()
|
||||
////
|
||||
//// private(set) var instructions: [String]
|
||||
//// @State private var currentInstructionIndex: Int = 0
|
||||
//// @State private var showPrevious: Bool = false
|
||||
//// @State private var showNext: Bool = false
|
||||
////
|
||||
//// var body: some View {
|
||||
//// SimpleInstructionsView(
|
||||
//// text: self.instructions[self.currentInstructionIndex],
|
||||
//// showPrevious: .init (
|
||||
//// get: {
|
||||
//// return (self.currentGlobalIndex > 0 && self.globalMaxCount > 0) || (self.currentInstructionIndex > 0 && self.instructions.count > 0)
|
||||
//// },
|
||||
//// set: { _ in}
|
||||
//// ),
|
||||
//// showNext: .init(
|
||||
//// get: {
|
||||
//// return self.currentInstructionIndex < instructions.count || self.currentGlobalIndex < self.globalMaxCount
|
||||
//// },
|
||||
//// set: { _ in }
|
||||
//// )
|
||||
//// )
|
||||
//// .onTapNext {
|
||||
//// guard currentInstructionIndex == instructions.count - 1 else {
|
||||
//// currentInstructionIndex += 1
|
||||
//// return
|
||||
//// }
|
||||
////
|
||||
//// advance()
|
||||
//// self.currentInstructionIndex = 0
|
||||
//// }
|
||||
//// .onTapPrev {
|
||||
//// guard currentInstructionIndex == 0 else {
|
||||
//// currentInstructionIndex -= 1
|
||||
//// return
|
||||
//// }
|
||||
////// guard self.currentInstructionIndex > 0 else {
|
||||
////// return
|
||||
////// }
|
||||
////// self.currentInstructionIndex -= 1
|
||||
//// self.currentInstructionIndex = 0
|
||||
//// previous()
|
||||
////// self.currentInstructionIndex = instructions.count
|
||||
////
|
||||
//// }
|
||||
//// .padding(.horizontal, 15)
|
||||
////// .onChange(of: self.currentInstructionIndex, perform: { newValue in
|
||||
////// // For the next button
|
||||
////// if(newValue < instructions.count - 1 || currentGlobalIndex < globalMaxCount - 1) {
|
||||
////// self.showNext = true
|
||||
////// } else {
|
||||
////// self.showNext = false
|
||||
////// }
|
||||
//////
|
||||
////// if(newValue > 0 && self.instructions.count > 0) {
|
||||
////// self.showPrevious = true
|
||||
////// } else {
|
||||
////// self.showPrevious = false
|
||||
////// }
|
||||
////// })
|
||||
//// .onAppear {
|
||||
//// // Next
|
||||
//// if(currentInstructionIndex < instructions.count - 1 || currentGlobalIndex < globalMaxCount) {
|
||||
//// self.showNext = true
|
||||
//// } else {
|
||||
//// self.showNext = false
|
||||
//// }
|
||||
////// if(currentInstructionIndex < instructions.count - 1) {
|
||||
////// self.showNext = true
|
||||
////// } else {
|
||||
////// self.showNext = false
|
||||
////// }
|
||||
//////
|
||||
//// if(currentInstructionIndex > 0 && self.instructions.count > 0) {
|
||||
//// self.showPrevious = true
|
||||
//// } else {
|
||||
//// self.showPrevious = false
|
||||
//// }
|
||||
//// }
|
||||
//// }
|
||||
////}
|
||||
////
|
||||
//////struct SimpleInstructionsViewWrapper_Previews: PreviewProvider {
|
||||
////// static var previews: some View {
|
||||
////// SimpleInstructionsViewWrapper(instructions: ["Hello", "World", "askdlfjalk shfjlka dhfkj ahsdfkhdj"])
|
||||
////// }
|
||||
//////}
|
|
@ -0,0 +1,147 @@
|
|||
// TODO: Create a default view
|
||||
////
|
||||
//// SimpleInstructionsView.swift
|
||||
////
|
||||
////
|
||||
//// Created by Muhand Jumah on 4/23/23.
|
||||
////
|
||||
//
|
||||
//import SwiftUI
|
||||
//
|
||||
//struct SimpleInstructionsView: View {
|
||||
// private var text: String
|
||||
// private var nextTapped: () -> ()
|
||||
// private var prevTapped: () -> ()
|
||||
//
|
||||
// @Binding var showPrevious: Bool
|
||||
// @Binding var showNext: Bool
|
||||
//
|
||||
// public init(
|
||||
// text: String,
|
||||
// showPrevious: Binding<Bool> = .constant(false),
|
||||
// showNext: Binding<Bool> = .constant(false)
|
||||
// ) {
|
||||
// self.text = text
|
||||
// self.nextTapped = { }
|
||||
// self.prevTapped = { }
|
||||
// self._showPrevious = showPrevious
|
||||
// self._showNext = showNext
|
||||
// }
|
||||
//
|
||||
// fileprivate init(
|
||||
// text: String,
|
||||
// nextTapped: @escaping () -> (),
|
||||
// prevTapped: @escaping () -> (),
|
||||
// showPrevious: Binding<Bool> = .constant(false),
|
||||
// showNext: Binding<Bool> = .constant(false)
|
||||
// ) {
|
||||
// self.text = text
|
||||
// self.nextTapped = nextTapped
|
||||
// self.prevTapped = prevTapped
|
||||
// self._showPrevious = showPrevious
|
||||
// self._showNext = showNext
|
||||
// }
|
||||
//
|
||||
// var body: some View {
|
||||
//// Color.purple
|
||||
// VStack(alignment:.leading, spacing: 20) {
|
||||
// HStack(alignment: .top, spacing: 10) {
|
||||
// Image(systemName: "lightbulb.circle.fill")
|
||||
// .font(.system(size: 32))
|
||||
// .foregroundColor(.blue)
|
||||
//
|
||||
// Text(self.text)
|
||||
// .font(.body)
|
||||
// }
|
||||
//
|
||||
// if (self.showPrevious || self.showNext) {
|
||||
// HStack(alignment: .center, spacing:15) {
|
||||
// Spacer()
|
||||
// if(self.showPrevious) {
|
||||
// Button(action: {
|
||||
// self.prevTapped()
|
||||
// }) {
|
||||
// Text("Previous")
|
||||
// .font(.callout)
|
||||
// .fontWeight(.medium)
|
||||
// .foregroundColor(.blue)
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// if(self.showNext) {
|
||||
//
|
||||
// Button(action: {
|
||||
// self.nextTapped()
|
||||
// }) {
|
||||
// Text("Next")
|
||||
// .font(.callout)
|
||||
// .fontWeight(.semibold)
|
||||
// .padding(.horizontal, 10)
|
||||
// .padding(.vertical, 5)
|
||||
// .background(Color.blue)
|
||||
// .cornerRadius(3)
|
||||
// .foregroundColor(.white)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// .padding(.horizontal, 20)
|
||||
// .padding(.vertical, 15)
|
||||
// .background(Color.white)
|
||||
// .cornerRadius(10)
|
||||
// .shadow(color: .black.opacity(0.2), radius: 5, x: 0, y: 0)
|
||||
// }
|
||||
//
|
||||
// func onTapNext(_ callback: @escaping () -> ()) -> Self {
|
||||
// return .init(
|
||||
// text: self.text,
|
||||
// nextTapped: callback,
|
||||
// prevTapped: self.prevTapped,
|
||||
// showPrevious: self.$showPrevious,
|
||||
// showNext: self.$showNext
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// func onTapPrev(_ callback: @escaping () -> ()) -> Self {
|
||||
// return .init(
|
||||
// text: self.text,
|
||||
// nextTapped: self.nextTapped,
|
||||
// prevTapped: callback,
|
||||
// showPrevious: self.$showPrevious,
|
||||
// showNext: self.$showNext
|
||||
// )
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//struct SimpleInstructionsView_Previews: PreviewProvider {
|
||||
// static var previews: some View {
|
||||
// ZStack {
|
||||
// Color.white
|
||||
// .edgesIgnoringSafeArea(.all)
|
||||
//
|
||||
// VStack(spacing: 5) {
|
||||
// SimpleInstructionsView(text: "This is just a tutorial asjdkfhla sdfhlakjsd hflkajsd hflkjasdhf lkjasdhf kljasd lajsdfh lkjsdhf lakjsdhf akjsl hfakjsd hadffajsdkfhlkajs hflkjas hfklja hsdjkfl haljskd fhljasdfhlaksd hflas dhfklj asdh fljasdlfjsdhfl.")
|
||||
//
|
||||
// SimpleInstructionsView(text: "This is just a tutorial asjdkfhla sdfhlakjsd hflkajsd hflkjasdhf lkjasdhf kljasd lajsdfh lkjsdhf lakjsdhf akjsl hfakjsd hadffajsdkfhlkajs hflkjas hfklja hsdjkfl haljskd fhljasdfhlaksd hflas dhfklj asdh fljasdlfjsdhfl.", showNext: .constant(true))
|
||||
// .onTapNext {
|
||||
// print("Next Tapped")
|
||||
// }
|
||||
//
|
||||
// SimpleInstructionsView(text: "This is just a tutorial asjdkfhla sdfhlakjsd hflkajsd hflkjasdhf lkjasdhf kljasd lajsdfh lkjsdhf lakjsdhf akjsl hfakjsd hadffajsdkfhlkajs hflkjas hfklja hsdjkfl haljskd fhljasdfhlaksd hflas dhfklj asdh fljasdlfjsdhfl.", showPrevious: .constant(true))
|
||||
// .onTapPrev {
|
||||
// print("Previous Tapped")
|
||||
// }
|
||||
//
|
||||
// SimpleInstructionsView(text: "This is just a tutorial asjdkfhla sdfhlakjsd hflkajsd hflkjasdhf lkjasdhf kljasd lajsdfh lkjsdhf lakjsdhf akjsl hfakjsd hadffajsdkfhlkajs hflkjas hfklja hsdjkfl haljskd fhljasdfhlaksd hflas dhfklj asdh fljasdlfjsdhfl.", showPrevious: .constant(true), showNext: .constant(true))
|
||||
// .onTapNext {
|
||||
// print("Next Tapped")
|
||||
// }
|
||||
// .onTapPrev {
|
||||
// print("Previous Tapped")
|
||||
// }
|
||||
// }
|
||||
// .padding()
|
||||
// }
|
||||
// }
|
||||
//}
|
|
@ -0,0 +1,46 @@
|
|||
//
|
||||
// SpotlightView.swift
|
||||
//
|
||||
//
|
||||
// Created by Muhand Jumah on 4/23/23.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
internal struct SpotlightView<Chunk: FlowChunk>: View {
|
||||
private var chunk: Chunk
|
||||
private var namespace: Namespace.ID
|
||||
private var namespace2: Namespace.ID
|
||||
|
||||
internal init(chunk: Chunk, namespace: Namespace.ID, namespace2: Namespace.ID) {
|
||||
self.chunk = chunk
|
||||
self.namespace = namespace
|
||||
self.namespace2 = namespace2
|
||||
}
|
||||
|
||||
internal var body: some View {
|
||||
ZStack {
|
||||
self.chunk.spotlightBackground.body(self.namespace)
|
||||
|
||||
|
||||
self.chunk.spotlightShape.body(self.namespace)
|
||||
.blendMode(.destinationOut)
|
||||
.matchedGeometryEffect(
|
||||
id: 10001,
|
||||
in: self.namespace2,
|
||||
properties: .position,
|
||||
anchor: self.chunk.instructionsViewPosition == .above ? .top : .bottom,
|
||||
isSource: true
|
||||
)
|
||||
|
||||
.matchedGeometryEffect(
|
||||
id: self.chunk.id,
|
||||
in: namespace,
|
||||
properties: .frame,
|
||||
anchor: .center,
|
||||
isSource: false
|
||||
)
|
||||
}
|
||||
.compositingGroup()
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue