diff --git a/GamebookEngine.xcodeproj/project.pbxproj b/GamebookEngine.xcodeproj/project.pbxproj index 920becd..44f6077 100644 --- a/GamebookEngine.xcodeproj/project.pbxproj +++ b/GamebookEngine.xcodeproj/project.pbxproj @@ -13,6 +13,8 @@ 440EF3F22323279D000A7C9F /* ContentSizedTableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 440EF3F12323279D000A7C9F /* ContentSizedTableView.swift */; }; 4417DC292314867C00A6F96B /* RuleEditorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4417DC272314867C00A6F96B /* RuleEditorViewController.swift */; }; 4417DC2A2314867C00A6F96B /* RuleEditorViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4417DC282314867C00A6F96B /* RuleEditorViewController.xib */; }; + 4433010929DE28D800A90F42 /* IntroductionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4433010829DE28D800A90F42 /* IntroductionView.swift */; }; + 4433010C29DE2A9A00A90F42 /* CardStack in Frameworks */ = {isa = PBXBuildFile; productRef = 4433010B29DE2A9A00A90F42 /* CardStack */; }; 4439E65C230B534D00C6A23C /* Game+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4439E652230B534D00C6A23C /* Game+CoreDataClass.swift */; }; 4439E65D230B534D00C6A23C /* Game+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4439E653230B534D00C6A23C /* Game+CoreDataProperties.swift */; }; 4439E65E230B534D00C6A23C /* Decision+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4439E654230B534D00C6A23C /* Decision+CoreDataClass.swift */; }; @@ -118,6 +120,7 @@ 440EF3F12323279D000A7C9F /* ContentSizedTableView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentSizedTableView.swift; sourceTree = ""; }; 4417DC272314867C00A6F96B /* RuleEditorViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RuleEditorViewController.swift; sourceTree = ""; }; 4417DC282314867C00A6F96B /* RuleEditorViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = RuleEditorViewController.xib; sourceTree = ""; }; + 4433010829DE28D800A90F42 /* IntroductionView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntroductionView.swift; sourceTree = ""; }; 4439E652230B534D00C6A23C /* Game+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Game+CoreDataClass.swift"; sourceTree = ""; }; 4439E653230B534D00C6A23C /* Game+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Game+CoreDataProperties.swift"; sourceTree = ""; }; 4439E654230B534D00C6A23C /* Decision+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Decision+CoreDataClass.swift"; sourceTree = ""; }; @@ -210,6 +213,7 @@ buildActionMask = 2147483647; files = ( 4407F51729DD21C400119893 /* MarkdownKit in Frameworks */, + 4433010C29DE2A9A00A90F42 /* CardStack in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -225,6 +229,14 @@ path = "Rule Editor"; sourceTree = ""; }; + 4433010729DE28C700A90F42 /* Introduction */ = { + isa = PBXGroup; + children = ( + 4433010829DE28D800A90F42 /* IntroductionView.swift */, + ); + path = Introduction; + sourceTree = ""; + }; 444358B4230C56FD00A2E004 /* Page List */ = { isa = PBXGroup; children = ( @@ -444,6 +456,7 @@ B4E9EFBA230A41AD00C9CA76 /* Views */ = { isa = PBXGroup; children = ( + 4433010729DE28C700A90F42 /* Introduction */, B4789FEF23188B8E008DBE9F /* Game List */, 444358C1230C7BA800A2E004 /* Playing */, 444358BC230C789900A2E004 /* Editing */, @@ -525,6 +538,7 @@ name = GamebookEngine; packageProductDependencies = ( 4407F51629DD21C400119893 /* MarkdownKit */, + 4433010B29DE2A9A00A90F42 /* CardStack */, ); productName = BRGamebookEngine; productReference = B448874B23063A0D000E2FDD /* GamebookEngine.app */; @@ -564,6 +578,7 @@ mainGroup = B448874223063A0D000E2FDD; packageReferences = ( 4407F51529DD21C400119893 /* XCRemoteSwiftPackageReference "MarkdownKit" */, + 4433010A29DE2A9A00A90F42 /* XCRemoteSwiftPackageReference "SwiftUI-CardStackView" */, ); productRefGroup = B448874C23063A0D000E2FDD /* Products */; projectDirPath = ""; @@ -656,6 +671,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 4433010929DE28D800A90F42 /* IntroductionView.swift in Sources */, 444358BF230C794400A2E004 /* PageEditorViewController.swift in Sources */, B452531D2314C186002F628C /* UserDatabase.swift in Sources */, B4F7C1482311AEB800EE055C /* PageEditorConsequenceTableViewCell.swift in Sources */, @@ -904,7 +920,7 @@ CURRENT_PROJECT_VERSION = 15; DEVELOPMENT_TEAM = 2Y9M69QJKZ; INFOPLIST_FILE = GamebookEngine/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -926,7 +942,7 @@ CURRENT_PROJECT_VERSION = 15; DEVELOPMENT_TEAM = 2Y9M69QJKZ; INFOPLIST_FILE = GamebookEngine/Info.plist; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.6; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -979,6 +995,14 @@ minimumVersion = 1.7.0; }; }; + 4433010A29DE2A9A00A90F42 /* XCRemoteSwiftPackageReference "SwiftUI-CardStackView" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/dadalar/SwiftUI-CardStackView.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 0.0.6; + }; + }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ @@ -987,6 +1011,11 @@ package = 4407F51529DD21C400119893 /* XCRemoteSwiftPackageReference "MarkdownKit" */; productName = MarkdownKit; }; + 4433010B29DE2A9A00A90F42 /* CardStack */ = { + isa = XCSwiftPackageProductDependency; + package = 4433010A29DE2A9A00A90F42 /* XCRemoteSwiftPackageReference "SwiftUI-CardStackView" */; + productName = CardStack; + }; /* End XCSwiftPackageProductDependency section */ /* Begin XCVersionGroup section */ diff --git a/GamebookEngine.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/GamebookEngine.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 48e6b7c..b86f134 100644 --- a/GamebookEngine.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/GamebookEngine.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -8,6 +8,24 @@ "revision" : "5056f3305d3499f44d8815530d560b87082e0cf5", "version" : "1.7.1" } + }, + { + "identity" : "swift-snapshot-testing", + "kind" : "remoteSourceControl", + "location" : "https://github.com/pointfreeco/swift-snapshot-testing.git", + "state" : { + "revision" : "cef5b3f6f11781dd4591bdd1dd0a3d22bd609334", + "version" : "1.11.0" + } + }, + { + "identity" : "swiftui-cardstackview", + "kind" : "remoteSourceControl", + "location" : "https://github.com/dadalar/SwiftUI-CardStackView.git", + "state" : { + "revision" : "9b45ef54b9e7bfbee46fe4a5982ecf449ac2b47c", + "version" : "0.0.6" + } } ], "version" : 2 diff --git a/GamebookEngine/Assets/Assets.xcassets/Contents.json b/GamebookEngine/Assets/Assets.xcassets/Contents.json index da4a164..73c0059 100644 --- a/GamebookEngine/Assets/Assets.xcassets/Contents.json +++ b/GamebookEngine/Assets/Assets.xcassets/Contents.json @@ -1,6 +1,6 @@ { "info" : { - "version" : 1, - "author" : "xcode" + "author" : "xcode", + "version" : 1 } -} \ No newline at end of file +} diff --git a/GamebookEngine/Assets/Assets.xcassets/create-icon.imageset/Contents.json b/GamebookEngine/Assets/Assets.xcassets/create-icon.imageset/Contents.json new file mode 100644 index 0000000..7f87359 --- /dev/null +++ b/GamebookEngine/Assets/Assets.xcassets/create-icon.imageset/Contents.json @@ -0,0 +1,16 @@ +{ + "images" : [ + { + "filename" : "create-icon.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true, + "template-rendering-intent" : "template" + } +} diff --git a/GamebookEngine/Assets/Assets.xcassets/create-icon.imageset/create-icon.pdf b/GamebookEngine/Assets/Assets.xcassets/create-icon.imageset/create-icon.pdf new file mode 100644 index 0000000..b31f2c2 Binary files /dev/null and b/GamebookEngine/Assets/Assets.xcassets/create-icon.imageset/create-icon.pdf differ diff --git a/GamebookEngine/Assets/Assets.xcassets/icon-ios.imageset/Contents.json b/GamebookEngine/Assets/Assets.xcassets/icon-ios.imageset/Contents.json new file mode 100644 index 0000000..739abb9 --- /dev/null +++ b/GamebookEngine/Assets/Assets.xcassets/icon-ios.imageset/Contents.json @@ -0,0 +1,12 @@ +{ + "images" : [ + { + "filename" : "icon-ios-1024@1x.png", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/GamebookEngine/Assets/Assets.xcassets/icon-ios.imageset/icon-ios-1024@1x.png b/GamebookEngine/Assets/Assets.xcassets/icon-ios.imageset/icon-ios-1024@1x.png new file mode 100644 index 0000000..5f343d2 Binary files /dev/null and b/GamebookEngine/Assets/Assets.xcassets/icon-ios.imageset/icon-ios-1024@1x.png differ diff --git a/GamebookEngine/Assets/Assets.xcassets/magic-icon.imageset/Contents.json b/GamebookEngine/Assets/Assets.xcassets/magic-icon.imageset/Contents.json new file mode 100644 index 0000000..1d080b6 --- /dev/null +++ b/GamebookEngine/Assets/Assets.xcassets/magic-icon.imageset/Contents.json @@ -0,0 +1,16 @@ +{ + "images" : [ + { + "filename" : "magic-icon.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true, + "template-rendering-intent" : "template" + } +} diff --git a/GamebookEngine/Assets/Assets.xcassets/magic-icon.imageset/magic-icon.pdf b/GamebookEngine/Assets/Assets.xcassets/magic-icon.imageset/magic-icon.pdf new file mode 100644 index 0000000..6d3919a Binary files /dev/null and b/GamebookEngine/Assets/Assets.xcassets/magic-icon.imageset/magic-icon.pdf differ diff --git a/GamebookEngine/Assets/Assets.xcassets/play-icon.imageset/Contents.json b/GamebookEngine/Assets/Assets.xcassets/play-icon.imageset/Contents.json new file mode 100644 index 0000000..9fb6812 --- /dev/null +++ b/GamebookEngine/Assets/Assets.xcassets/play-icon.imageset/Contents.json @@ -0,0 +1,16 @@ +{ + "images" : [ + { + "filename" : "play-icon.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true, + "template-rendering-intent" : "template" + } +} diff --git a/GamebookEngine/Assets/Assets.xcassets/play-icon.imageset/play-icon.pdf b/GamebookEngine/Assets/Assets.xcassets/play-icon.imageset/play-icon.pdf new file mode 100644 index 0000000..8fe14db Binary files /dev/null and b/GamebookEngine/Assets/Assets.xcassets/play-icon.imageset/play-icon.pdf differ diff --git a/GamebookEngine/Assets/Assets.xcassets/share-icon.imageset/Contents.json b/GamebookEngine/Assets/Assets.xcassets/share-icon.imageset/Contents.json new file mode 100644 index 0000000..a0e304b --- /dev/null +++ b/GamebookEngine/Assets/Assets.xcassets/share-icon.imageset/Contents.json @@ -0,0 +1,16 @@ +{ + "images" : [ + { + "filename" : "share-icon.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true, + "template-rendering-intent" : "template" + } +} diff --git a/GamebookEngine/Assets/Assets.xcassets/share-icon.imageset/share-icon.pdf b/GamebookEngine/Assets/Assets.xcassets/share-icon.imageset/share-icon.pdf new file mode 100644 index 0000000..1eeafd5 Binary files /dev/null and b/GamebookEngine/Assets/Assets.xcassets/share-icon.imageset/share-icon.pdf differ diff --git a/GamebookEngine/Assets/Assets.xcassets/welcome-icon.imageset/Contents.json b/GamebookEngine/Assets/Assets.xcassets/welcome-icon.imageset/Contents.json new file mode 100644 index 0000000..fd34f6c --- /dev/null +++ b/GamebookEngine/Assets/Assets.xcassets/welcome-icon.imageset/Contents.json @@ -0,0 +1,16 @@ +{ + "images" : [ + { + "filename" : "welcome-icon.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "preserves-vector-representation" : true, + "template-rendering-intent" : "template" + } +} diff --git a/GamebookEngine/Assets/Assets.xcassets/welcome-icon.imageset/welcome-icon.pdf b/GamebookEngine/Assets/Assets.xcassets/welcome-icon.imageset/welcome-icon.pdf new file mode 100644 index 0000000..4a0212c Binary files /dev/null and b/GamebookEngine/Assets/Assets.xcassets/welcome-icon.imageset/welcome-icon.pdf differ diff --git a/GamebookEngine/Data/UserDatabase.swift b/GamebookEngine/Data/UserDatabase.swift index dd28433..5520b69 100644 --- a/GamebookEngine/Data/UserDatabase.swift +++ b/GamebookEngine/Data/UserDatabase.swift @@ -11,6 +11,7 @@ import UIKit extension String { static let createdIntro = "createdIntro" + static let shownIntroductionScreen = "shownIntroductionScreen" } struct UserDatabase { @@ -21,6 +22,11 @@ struct UserDatabase { } extension UserDefaults { + + func shouldShowIntroductoryScreen() -> Bool { + return !bool(forKey: .shownIntroductionScreen) + } + func createIntroGameIfNeeded() { let status: Bool = bool(forKey: .createdIntro) if !status, let introURL = Bundle.main.url(forResource: "An Introduction to Gamebook Engine", withExtension: "gbook"), diff --git a/GamebookEngine/Views/Game List/GameListTableViewController.swift b/GamebookEngine/Views/Game List/GameListTableViewController.swift index 10aa725..fa3b6c1 100644 --- a/GamebookEngine/Views/Game List/GameListTableViewController.swift +++ b/GamebookEngine/Views/Game List/GameListTableViewController.swift @@ -7,6 +7,8 @@ // import UIKit +import SwiftUI +import UniformTypeIdentifiers class GameListTableViewController: UITableViewController { var games: [Game] = [] @@ -55,6 +57,7 @@ class GameListTableViewController: UITableViewController { super.viewDidAppear(animated) fetchGames() + showIntroductionScreen() } override func viewDidLayoutSubviews() { @@ -119,7 +122,14 @@ extension GameListTableViewController: GameListGameTableViewCellDelegate, UIDocu } @objc fileprivate func importGame() { - let documentPicker = UIDocumentPickerViewController(documentTypes: ["net.amiantos.BRGamebookEngine.gbook"], in: .import) + var documentPicker: UIDocumentPickerViewController! + if #available(iOS 14, *) { + let supportedTypes: [UTType] = [UTType("net.amiantos.BRGamebookEngine.gbook")!] + documentPicker = UIDocumentPickerViewController(forOpeningContentTypes: supportedTypes) + } else { + let supportedTypes: [String] = ["net.amiantos.BRGamebookEngine.gbook"] + documentPicker = UIDocumentPickerViewController(documentTypes: supportedTypes, in: .import) + } documentPicker.delegate = self documentPicker.modalPresentationStyle = .formSheet present(documentPicker, animated: true, completion: nil) @@ -161,6 +171,15 @@ extension GameListTableViewController: GameListGameTableViewCellDelegate, UIDocu } } + @objc fileprivate func showIntroductionScreen() { + if UserDatabase.standard.shouldShowIntroductoryScreen() { + let swiftUIViewController = UIHostingController(rootView: IntroductionView()) + swiftUIViewController.modalPresentationStyle = .pageSheet + swiftUIViewController.isModalInPresentation = true + present(swiftUIViewController, animated: true, completion: nil) + } + } + fileprivate func showFilePicker(_ sender: UIButton) { let actionSheet = UIAlertController(title: "Add Game", message: nil, preferredStyle: .actionSheet) let importAction = UIAlertAction(title: "Import from file", style: .default) { _ in diff --git a/GamebookEngine/Views/Introduction/IntroductionView.swift b/GamebookEngine/Views/Introduction/IntroductionView.swift new file mode 100644 index 0000000..6ef122f --- /dev/null +++ b/GamebookEngine/Views/Introduction/IntroductionView.swift @@ -0,0 +1,145 @@ +// +// IntroductionView.swift +// GamebookEngine +// +// Created by Brad Root on 4/5/23. +// Copyright © 2023 Brad Root. All rights reserved. +// + +import CardStack +import SwiftUI + +struct CardData: Identifiable { + var id = UUID() + let title: String + let text: String + let image: Image +} + +struct CardView: View { + let data: CardData + + var body: some View { + VStack { + Text(self.data.title) + .font(.title) + .minimumScaleFactor(0.5) + .padding() + Spacer() + GeometryReader { geo in + self.data.image + .resizable() + .scaledToFit() + .frame(width: geo.size.width * 0.8) + .frame(width: geo.size.width, height: geo.size.height) + } + Spacer() + Text(self.data.text) + .lineLimit(4) + .font(.body) + .minimumScaleFactor(0.5) + .padding(24) + } + .background(Color(UIColor(white: 0.95, alpha: 1))) + .foregroundColor(Color.black) + .cornerRadius(10) + .shadow(radius: 4) + } +} + +struct IntroductionView: View { + @Environment(\.dismiss) var dismiss + + @State var cards: [CardData] = [ + // CardData( +// title: "Salutations", text: "Welcome to Gamebook Engine, the all-in-one app for creating " +// + "and playing interactive stories, entirely on your phone or " +// + "tablet. No account needed, no cloud services, total privacy.", image: Image("magic-icon") +// ), + CardData( + title: "Create", text: "Edit gamebooks or create your own! Gamebook Engine has " + + "a fully-featured interface for creating full length choosable path adventure " + + "games.", image: Image("create-icon") + ), + CardData( + title: "Share", text: "Stories written with Gamebook Engine can be exported to a " + + "plain text file that you can send to your friends. To play, they " + + "just need their own copy of Gamebook Engine.", image: Image("share-icon") + ), + CardData( + title: "Play", text: "Gamebook Engine comes " + + "with a few simple games built in that you can play, but it’s more fun to play " + + "games made by friends and family. Get creative!", image: Image("play-icon") + ), + ] + + var body: some View { + VStack { + if UIDevice.current.userInterfaceIdiom == .phone { + Spacer() + } + Spacer() + HStack { + Image("icon-ios") + .resizable() + .scaledToFit() + .frame(width: 80, height: 80) + .cornerRadius(15) + .shadow(radius: 5, x: 2, y: 2) + .padding(.trailing, 10) + Text("Welcome to \nGamebook Engine") + .multilineTextAlignment(.leading) + .font(.title) + } + Text("Gamebook Engine is an all-in-one app for creating " + + "and playing interactive stories, entirely on your device. " + + "No sign-up or internet access required.") + .font(.body) + .multilineTextAlignment(.center) + .minimumScaleFactor(0.5) + .padding(.horizontal, 32) + .padding(.vertical, 12) + .frame(maxWidth: 540) + CardStack( + direction: LeftRight.direction, + data: cards, + onSwipe: { text, direction in + print("Swiped \(text) to \(direction)") + self.cards.append(text) + }, + content: { data, _, _ in + CardView(data: data) + } + ) + .padding() + .scaledToFit() + .frame(alignment: .center) + .frame(maxWidth: 400) + Spacer() + Button(action: { + self.dismiss() + }, label: { + Text("Get Started Now") + .padding() + .background(Color(UIColor(named: "containerBackground")!)) + .foregroundColor(Color(UIColor(named: "text")!)) + .cornerRadius(100) + }) + Spacer() + if UIDevice.current.userInterfaceIdiom == .phone || UIDevice.current.orientation == .portrait { + Spacer() + } + } + .background(Color(UIColor(named: "background")!)) + .ignoresSafeArea(.all, edges: .vertical) + .onAppear { + UserDatabase.standard.set(true, forKey: .shownIntroductionScreen) + } + } +} + +struct IntroductionView_Previews: PreviewProvider { + static var previews: some View { + IntroductionView() + } +}