diff --git a/0245-tca-tour-pt3/README.md b/0245-tca-tour-pt3/README.md index 0fbac4ed..25ace24a 100644 --- a/0245-tca-tour-pt3/README.md +++ b/0245-tca-tour-pt3/README.md @@ -1,5 +1,5 @@ ## [Point-Free](https://www.pointfree.co) -> #### This directory contains code from Point-Free Episode: [Tour of the Composable Architecture 1.0: Navigation](https://www.pointfree.co/episodes/ep244-tour-of-the-composable-architecture-1-0-navigation) +> #### This directory contains code from Point-Free Episode: [Tour of the Composable Architecture 1.0: Navigation](https://www.pointfree.co/episodes/ep245-tour-of-the-composable-architecture-1-0-navigation) > > With the standups list and standup form features ready, it’s time to integrate them together using the Composable Architecture’s navigation tools. We will make it so you can add and edit standups via a sheet, and write comprehensive unit tests for this integration. diff --git a/0246-tca-tour-pt4/README.md b/0246-tca-tour-pt4/README.md index bd9f6f6f..8ed1a78f 100644 --- a/0246-tca-tour-pt4/README.md +++ b/0246-tca-tour-pt4/README.md @@ -1,5 +1,5 @@ ## [Point-Free](https://www.pointfree.co) -> #### This directory contains code from Point-Free Episode: [Tour of the Composable Architecture 1.0: Stacks](https://www.pointfree.co/episodes/ep244-tour-of-the-composable-architecture-1-0-stacks) +> #### This directory contains code from Point-Free Episode: [Tour of the Composable Architecture 1.0: Stacks](https://www.pointfree.co/episodes/ep246-tour-of-the-composable-architecture-1-0-stacks) > > We show how to add stack-based navigation to a Composable Architecture application, how to support many different kinds of screens, how to deep link into a navigation stack, and how to write deep tests for how navigation is integrated into the application. diff --git a/0247-tca-tour-pt5/README.md b/0247-tca-tour-pt5/README.md new file mode 100644 index 00000000..35911136 --- /dev/null +++ b/0247-tca-tour-pt5/README.md @@ -0,0 +1,5 @@ +## [Point-Free](https://www.pointfree.co) + +> #### This directory contains code from Point-Free Episode: [Tour of the Composable Architecture 1.0: Correctness](https://www.pointfree.co/episodes/ep247-tour-of-the-composable-architecture-1-0-correctness) +> +> We’ll learn how to precisely model navigation in the Composable Architecture using an enum to eliminate impossible runtime states at compile time. And we’ll begin to implement the app’s most complex screen and most complex dependency: the record meeting view and the speech client. diff --git a/0247-tca-tour-pt5/Scrumdinger-Complete/Configuration/SampleCode.xcconfig b/0247-tca-tour-pt5/Scrumdinger-Complete/Configuration/SampleCode.xcconfig new file mode 100644 index 00000000..db86c069 --- /dev/null +++ b/0247-tca-tour-pt5/Scrumdinger-Complete/Configuration/SampleCode.xcconfig @@ -0,0 +1,13 @@ +// +// See LICENSE folder for this sample’s licensing information. +// +// SampleCode.xcconfig +// + +// The `SAMPLE_CODE_DISAMBIGUATOR` configuration is to make it easier to build +// and run a sample code project. Once you set your project's development team, +// you'll have a unique bundle identifier. This is because the bundle identifier +// is derived based on the 'SAMPLE_CODE_DISAMBIGUATOR' value. Do not use this +// approach in your own projects—it's only useful for sample code projects because +// they are frequently downloaded and don't have a development team set. +SAMPLE_CODE_DISAMBIGUATOR=${DEVELOPMENT_TEAM} diff --git a/0247-tca-tour-pt5/Scrumdinger-Complete/LICENSE/LICENSE.txt b/0247-tca-tour-pt5/Scrumdinger-Complete/LICENSE/LICENSE.txt new file mode 100644 index 00000000..9e79b9ef --- /dev/null +++ b/0247-tca-tour-pt5/Scrumdinger-Complete/LICENSE/LICENSE.txt @@ -0,0 +1,7 @@ +Copyright © 2022 Apple Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/0247-tca-tour-pt5/Scrumdinger-Complete/README.md b/0247-tca-tour-pt5/Scrumdinger-Complete/README.md new file mode 100644 index 00000000..03bf0dd1 --- /dev/null +++ b/0247-tca-tour-pt5/Scrumdinger-Complete/README.md @@ -0,0 +1,5 @@ +# Transcribing Speech to Text + +## Completed Project + +Explore the completed project for [Transcribing Speech to Text](https://developer.apple.com/tutorials/app-dev-training/transcribing-speech-to-text). \ No newline at end of file diff --git a/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger.xcodeproj/.xcodesamplecode.plist b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger.xcodeproj/.xcodesamplecode.plist new file mode 100644 index 00000000..4c2052dd --- /dev/null +++ b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger.xcodeproj/.xcodesamplecode.plist @@ -0,0 +1,7 @@ + + + + + + + diff --git a/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger.xcodeproj/project.pbxproj b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger.xcodeproj/project.pbxproj new file mode 100644 index 00000000..174a4eac --- /dev/null +++ b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger.xcodeproj/project.pbxproj @@ -0,0 +1,486 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 56; + objects = { + +/* Begin PBXBuildFile section */ + A93E3F39294A6F3400B9708D /* ScrumProgressViewStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = A93E3F38294A6F3400B9708D /* ScrumProgressViewStyle.swift */; }; + A93E3F3B294A6F4200B9708D /* ScrumTimer.swift in Sources */ = {isa = PBXBuildFile; fileRef = A93E3F3A294A6F4200B9708D /* ScrumTimer.swift */; }; + A93E3F3D294A716700B9708D /* MeetingHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A93E3F3C294A715B00B9708D /* MeetingHeaderView.swift */; }; + A93E3F3F294A748B00B9708D /* MeetingFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A93E3F3E294A748B00B9708D /* MeetingFooterView.swift */; }; + A93E3F41294A912B00B9708D /* History.swift in Sources */ = {isa = PBXBuildFile; fileRef = A93E3F40294A912B00B9708D /* History.swift */; }; + A93E3F43294D18F700B9708D /* ErrorWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = A93E3F42294D18F700B9708D /* ErrorWrapper.swift */; }; + A93E3F45294D196600B9708D /* ErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A93E3F44294D196600B9708D /* ErrorView.swift */; }; + A93E3F47294D1E5600B9708D /* MeetingTimerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A93E3F46294D1E5600B9708D /* MeetingTimerView.swift */; }; + A93E3F49294D1F8500B9708D /* SpeakerArc.swift in Sources */ = {isa = PBXBuildFile; fileRef = A93E3F48294D1F8500B9708D /* SpeakerArc.swift */; }; + A93E3F4B294D224A00B9708D /* SpeechRecognizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = A93E3F4A294D224A00B9708D /* SpeechRecognizer.swift */; }; + A93E3F4D294D28C400B9708D /* HistoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A93E3F4C294D28C400B9708D /* HistoryView.swift */; }; + A9789BAE2947DE5100305A2F /* Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9789BAD2947DE5100305A2F /* Theme.swift */; }; + A9789BB02947DF6300305A2F /* DailyScrum.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9789BAF2947DF6300305A2F /* DailyScrum.swift */; }; + A9789BB22947E08F00305A2F /* CardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9789BB12947E08F00305A2F /* CardView.swift */; }; + A9789BB52947E51500305A2F /* TrailingIconLabelStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9789BB42947E51500305A2F /* TrailingIconLabelStyle.swift */; }; + AA313BB629B69F0A00F4309A /* ding.wav in Resources */ = {isa = PBXBuildFile; fileRef = AA313BB529B69F0A00F4309A /* ding.wav */; }; + AA7F3039294A878E005E1E9F /* AVPlayer+Ding.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA7F3038294A878E005E1E9F /* AVPlayer+Ding.swift */; }; + AAA368B729957D1A00FE35E9 /* NewScrumSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAA368B629957D1A00FE35E9 /* NewScrumSheet.swift */; }; + AAAE8020294793AB0099DABC /* ScrumdingerApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAAE801F294793AB0099DABC /* ScrumdingerApp.swift */; }; + AAAE8022294793AB0099DABC /* MeetingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AAAE8021294793AB0099DABC /* MeetingView.swift */; }; + AAAE8024294793AB0099DABC /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = AAAE8023294793AB0099DABC /* Assets.xcassets */; }; + AAAE8027294793AB0099DABC /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = AAAE8026294793AB0099DABC /* Preview Assets.xcassets */; }; + AAAE8031294795050099DABC /* SampleCode.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = AAAE8030294795050099DABC /* SampleCode.xcconfig */; }; + C768FBAF294B7F8300798D32 /* ScrumStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = C768FBAE294B7F8300798D32 /* ScrumStore.swift */; }; + C76970A02948F3A4002748F5 /* ScrumsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C769709F2948F3A4002748F5 /* ScrumsView.swift */; }; + C76970A22948F9CA002748F5 /* DetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C76970A12948F9CA002748F5 /* DetailView.swift */; }; + C76970A629491AB2002748F5 /* DetailEditView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C76970A529491AB2002748F5 /* DetailEditView.swift */; }; + C76970A829493341002748F5 /* ThemeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C76970A729493341002748F5 /* ThemeView.swift */; }; + C76970AA29493621002748F5 /* ThemePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = C76970A929493621002748F5 /* ThemePicker.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + A93E3F38294A6F3400B9708D /* ScrumProgressViewStyle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScrumProgressViewStyle.swift; sourceTree = ""; }; + A93E3F3A294A6F4200B9708D /* ScrumTimer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ScrumTimer.swift; path = Scrumdinger/Models/ScrumTimer.swift; sourceTree = SOURCE_ROOT; }; + A93E3F3C294A715B00B9708D /* MeetingHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeetingHeaderView.swift; sourceTree = ""; }; + A93E3F3E294A748B00B9708D /* MeetingFooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeetingFooterView.swift; sourceTree = ""; }; + A93E3F40294A912B00B9708D /* History.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = History.swift; sourceTree = ""; }; + A93E3F42294D18F700B9708D /* ErrorWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorWrapper.swift; sourceTree = ""; }; + A93E3F44294D196600B9708D /* ErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorView.swift; sourceTree = ""; }; + A93E3F46294D1E5600B9708D /* MeetingTimerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeetingTimerView.swift; sourceTree = ""; }; + A93E3F48294D1F8500B9708D /* SpeakerArc.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpeakerArc.swift; sourceTree = ""; }; + A93E3F4A294D224A00B9708D /* SpeechRecognizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpeechRecognizer.swift; sourceTree = ""; }; + A93E3F4C294D28C400B9708D /* HistoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryView.swift; sourceTree = ""; }; + A9789BAD2947DE5100305A2F /* Theme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Theme.swift; sourceTree = ""; }; + A9789BAF2947DF6300305A2F /* DailyScrum.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DailyScrum.swift; sourceTree = ""; }; + A9789BB12947E08F00305A2F /* CardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardView.swift; sourceTree = ""; }; + A9789BB42947E51500305A2F /* TrailingIconLabelStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrailingIconLabelStyle.swift; sourceTree = ""; }; + AA313BB529B69F0A00F4309A /* ding.wav */ = {isa = PBXFileReference; lastKnownFileType = audio.wav; path = ding.wav; sourceTree = ""; }; + AA7F3038294A878E005E1E9F /* AVPlayer+Ding.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "AVPlayer+Ding.swift"; sourceTree = ""; }; + AAA368B629957D1A00FE35E9 /* NewScrumSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewScrumSheet.swift; sourceTree = ""; }; + AAAE801C294793AB0099DABC /* Scrumdinger.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Scrumdinger.app; sourceTree = BUILT_PRODUCTS_DIR; }; + AAAE801F294793AB0099DABC /* ScrumdingerApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrumdingerApp.swift; sourceTree = ""; }; + AAAE8021294793AB0099DABC /* MeetingView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MeetingView.swift; sourceTree = ""; }; + AAAE8023294793AB0099DABC /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + AAAE8026294793AB0099DABC /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; + AAAE802D2947945C0099DABC /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; + AAAE802E2947949E0099DABC /* LICENSE */ = {isa = PBXFileReference; lastKnownFileType = folder; path = LICENSE; sourceTree = ""; }; + AAAE8030294795050099DABC /* SampleCode.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = SampleCode.xcconfig; sourceTree = ""; }; + C768FBAE294B7F8300798D32 /* ScrumStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrumStore.swift; sourceTree = ""; }; + C769709F2948F3A4002748F5 /* ScrumsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrumsView.swift; sourceTree = ""; }; + C76970A12948F9CA002748F5 /* DetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailView.swift; sourceTree = ""; }; + C76970A529491AB2002748F5 /* DetailEditView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailEditView.swift; sourceTree = ""; }; + C76970A729493341002748F5 /* ThemeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeView.swift; sourceTree = ""; }; + C76970A929493621002748F5 /* ThemePicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemePicker.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + AAAE8019294793AB0099DABC /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + A9789BAC2947DE3900305A2F /* Models */ = { + isa = PBXGroup; + children = ( + AA7F3038294A878E005E1E9F /* AVPlayer+Ding.swift */, + A9789BAF2947DF6300305A2F /* DailyScrum.swift */, + A93E3F42294D18F700B9708D /* ErrorWrapper.swift */, + A93E3F40294A912B00B9708D /* History.swift */, + C768FBAE294B7F8300798D32 /* ScrumStore.swift */, + A93E3F3A294A6F4200B9708D /* ScrumTimer.swift */, + A93E3F4A294D224A00B9708D /* SpeechRecognizer.swift */, + A9789BAD2947DE5100305A2F /* Theme.swift */, + ); + path = Models; + sourceTree = ""; + }; + A9789BB32947E4F500305A2F /* Views */ = { + isa = PBXGroup; + children = ( + A9789BB12947E08F00305A2F /* CardView.swift */, + C76970A529491AB2002748F5 /* DetailEditView.swift */, + C76970A12948F9CA002748F5 /* DetailView.swift */, + A93E3F44294D196600B9708D /* ErrorView.swift */, + A93E3F4C294D28C400B9708D /* HistoryView.swift */, + A93E3F3E294A748B00B9708D /* MeetingFooterView.swift */, + A93E3F3C294A715B00B9708D /* MeetingHeaderView.swift */, + A93E3F46294D1E5600B9708D /* MeetingTimerView.swift */, + AAAE8021294793AB0099DABC /* MeetingView.swift */, + AAA368B629957D1A00FE35E9 /* NewScrumSheet.swift */, + A93E3F38294A6F3400B9708D /* ScrumProgressViewStyle.swift */, + C769709F2948F3A4002748F5 /* ScrumsView.swift */, + A93E3F48294D1F8500B9708D /* SpeakerArc.swift */, + A9789BB42947E51500305A2F /* TrailingIconLabelStyle.swift */, + C76970A929493621002748F5 /* ThemePicker.swift */, + C76970A729493341002748F5 /* ThemeView.swift */, + ); + path = Views; + sourceTree = ""; + }; + AA313BB429B69EDC00F4309A /* Resources */ = { + isa = PBXGroup; + children = ( + AA313BB529B69F0A00F4309A /* ding.wav */, + ); + path = Resources; + sourceTree = ""; + }; + AAAE8013294793AB0099DABC = { + isa = PBXGroup; + children = ( + AAAE802D2947945C0099DABC /* README.md */, + AAAE801E294793AB0099DABC /* Scrumdinger */, + AAAE802F294794D40099DABC /* Configuration */, + AAAE802E2947949E0099DABC /* LICENSE */, + AAAE801D294793AB0099DABC /* Products */, + ); + sourceTree = ""; + }; + AAAE801D294793AB0099DABC /* Products */ = { + isa = PBXGroup; + children = ( + AAAE801C294793AB0099DABC /* Scrumdinger.app */, + ); + name = Products; + sourceTree = ""; + }; + AAAE801E294793AB0099DABC /* Scrumdinger */ = { + isa = PBXGroup; + children = ( + A9789BAC2947DE3900305A2F /* Models */, + A9789BB32947E4F500305A2F /* Views */, + AAAE801F294793AB0099DABC /* ScrumdingerApp.swift */, + AAAE8023294793AB0099DABC /* Assets.xcassets */, + AA313BB429B69EDC00F4309A /* Resources */, + AAAE8025294793AB0099DABC /* Preview Content */, + ); + path = Scrumdinger; + sourceTree = ""; + }; + AAAE8025294793AB0099DABC /* Preview Content */ = { + isa = PBXGroup; + children = ( + AAAE8026294793AB0099DABC /* Preview Assets.xcassets */, + ); + path = "Preview Content"; + sourceTree = ""; + }; + AAAE802F294794D40099DABC /* Configuration */ = { + isa = PBXGroup; + children = ( + AAAE8030294795050099DABC /* SampleCode.xcconfig */, + ); + path = Configuration; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + AAAE801B294793AB0099DABC /* Scrumdinger */ = { + isa = PBXNativeTarget; + buildConfigurationList = AAAE802A294793AB0099DABC /* Build configuration list for PBXNativeTarget "Scrumdinger" */; + buildPhases = ( + AAAE8018294793AB0099DABC /* Sources */, + AAAE8019294793AB0099DABC /* Frameworks */, + AAAE801A294793AB0099DABC /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Scrumdinger; + productName = Scrumdinger; + productReference = AAAE801C294793AB0099DABC /* Scrumdinger.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + AAAE8014294793AB0099DABC /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1500; + LastUpgradeCheck = 1410; + TargetAttributes = { + AAAE801B294793AB0099DABC = { + CreatedOnToolsVersion = 14.1; + }; + }; + }; + buildConfigurationList = AAAE8017294793AB0099DABC /* Build configuration list for PBXProject "Scrumdinger" */; + compatibilityVersion = "Xcode 14.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = AAAE8013294793AB0099DABC; + productRefGroup = AAAE801D294793AB0099DABC /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + AAAE801B294793AB0099DABC /* Scrumdinger */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + AAAE801A294793AB0099DABC /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + AAAE8027294793AB0099DABC /* Preview Assets.xcassets in Resources */, + AAAE8031294795050099DABC /* SampleCode.xcconfig in Resources */, + AA313BB629B69F0A00F4309A /* ding.wav in Resources */, + AAAE8024294793AB0099DABC /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + AAAE8018294793AB0099DABC /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + A93E3F41294A912B00B9708D /* History.swift in Sources */, + A93E3F4D294D28C400B9708D /* HistoryView.swift in Sources */, + C76970AA29493621002748F5 /* ThemePicker.swift in Sources */, + A9789BB02947DF6300305A2F /* DailyScrum.swift in Sources */, + A93E3F3F294A748B00B9708D /* MeetingFooterView.swift in Sources */, + C768FBAF294B7F8300798D32 /* ScrumStore.swift in Sources */, + A93E3F39294A6F3400B9708D /* ScrumProgressViewStyle.swift in Sources */, + C76970A22948F9CA002748F5 /* DetailView.swift in Sources */, + C76970A629491AB2002748F5 /* DetailEditView.swift in Sources */, + A9789BB52947E51500305A2F /* TrailingIconLabelStyle.swift in Sources */, + A93E3F47294D1E5600B9708D /* MeetingTimerView.swift in Sources */, + A9789BAE2947DE5100305A2F /* Theme.swift in Sources */, + A93E3F43294D18F700B9708D /* ErrorWrapper.swift in Sources */, + A93E3F3D294A716700B9708D /* MeetingHeaderView.swift in Sources */, + C76970A02948F3A4002748F5 /* ScrumsView.swift in Sources */, + AAAE8022294793AB0099DABC /* MeetingView.swift in Sources */, + AA7F3039294A878E005E1E9F /* AVPlayer+Ding.swift in Sources */, + A93E3F4B294D224A00B9708D /* SpeechRecognizer.swift in Sources */, + A93E3F45294D196600B9708D /* ErrorView.swift in Sources */, + AAAE8020294793AB0099DABC /* ScrumdingerApp.swift in Sources */, + C76970A829493341002748F5 /* ThemeView.swift in Sources */, + AAA368B729957D1A00FE35E9 /* NewScrumSheet.swift in Sources */, + A9789BB22947E08F00305A2F /* CardView.swift in Sources */, + A93E3F49294D1F8500B9708D /* SpeakerArc.swift in Sources */, + A93E3F3B294A6F4200B9708D /* ScrumTimer.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + AAAE8028294793AB0099DABC /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = AAAE8030294795050099DABC /* SampleCode.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 16.1; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + AAAE8029294793AB0099DABC /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = AAAE8030294795050099DABC /* SampleCode.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 16.1; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + AAAE802B294793AB0099DABC /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = AAAE8030294795050099DABC /* SampleCode.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"Scrumdinger/Preview Content\""; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSMicrophoneUsageDescription = "Audio is recorded to transcribe the meeting. Audio recordings are discarded after transcription."; + INFOPLIST_KEY_NSSpeechRecognitionUsageDescription = "You can view a text transcription of your meeting in the app."; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.Scrumdinger"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + AAAE802C294793AB0099DABC /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = AAAE8030294795050099DABC /* SampleCode.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"Scrumdinger/Preview Content\""; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSMicrophoneUsageDescription = "Audio is recorded to transcribe the meeting. Audio recordings are discarded after transcription."; + INFOPLIST_KEY_NSSpeechRecognitionUsageDescription = "You can view a text transcription of your meeting in the app."; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = "com.example.apple-samplecode.Scrumdinger"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + AAAE8017294793AB0099DABC /* Build configuration list for PBXProject "Scrumdinger" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + AAAE8028294793AB0099DABC /* Debug */, + AAAE8029294793AB0099DABC /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + AAAE802A294793AB0099DABC /* Build configuration list for PBXNativeTarget "Scrumdinger" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + AAAE802B294793AB0099DABC /* Debug */, + AAAE802C294793AB0099DABC /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = AAAE8014294793AB0099DABC /* Project object */; +} diff --git a/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..919434a6 --- /dev/null +++ b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger.xcodeproj/xcshareddata/xcschemes/Scrumdinger.xcscheme b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger.xcodeproj/xcshareddata/xcschemes/Scrumdinger.xcscheme new file mode 100644 index 00000000..f43fcddc --- /dev/null +++ b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger.xcodeproj/xcshareddata/xcschemes/Scrumdinger.xcscheme @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Assets.xcassets/AccentColor.colorset/Contents.json b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 00000000..eb878970 --- /dev/null +++ b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Assets.xcassets/AppIcon.appiconset/AppIcon1024@1x.png b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Assets.xcassets/AppIcon.appiconset/AppIcon1024@1x.png new file mode 100644 index 00000000..46ca82d6 Binary files /dev/null and b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Assets.xcassets/AppIcon.appiconset/AppIcon1024@1x.png differ diff --git a/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Assets.xcassets/AppIcon.appiconset/AppIcon20@1x.png b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Assets.xcassets/AppIcon.appiconset/AppIcon20@1x.png new file mode 100644 index 00000000..f35a2253 Binary files /dev/null and b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Assets.xcassets/AppIcon.appiconset/AppIcon20@1x.png differ diff --git a/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Assets.xcassets/AppIcon.appiconset/AppIcon20@2x-1.png b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Assets.xcassets/AppIcon.appiconset/AppIcon20@2x-1.png new file mode 100644 index 00000000..cd71898e Binary files /dev/null and b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Assets.xcassets/AppIcon.appiconset/AppIcon20@2x-1.png differ diff --git a/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Assets.xcassets/AppIcon.appiconset/AppIcon20@2x.png b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Assets.xcassets/AppIcon.appiconset/AppIcon20@2x.png new file mode 100644 index 00000000..cd71898e Binary files /dev/null and b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Assets.xcassets/AppIcon.appiconset/AppIcon20@2x.png differ diff --git a/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Assets.xcassets/AppIcon.appiconset/AppIcon20@3x.png b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Assets.xcassets/AppIcon.appiconset/AppIcon20@3x.png new file mode 100644 index 00000000..7e8ad93e Binary files /dev/null and b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Assets.xcassets/AppIcon.appiconset/AppIcon20@3x.png differ diff --git a/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Assets.xcassets/AppIcon.appiconset/AppIcon29@1x.png b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Assets.xcassets/AppIcon.appiconset/AppIcon29@1x.png new file mode 100644 index 00000000..2849afdb Binary files /dev/null and b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Assets.xcassets/AppIcon.appiconset/AppIcon29@1x.png differ diff --git a/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Assets.xcassets/AppIcon.appiconset/AppIcon29@2x-1.png b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Assets.xcassets/AppIcon.appiconset/AppIcon29@2x-1.png new file mode 100644 index 00000000..72064904 Binary files /dev/null and b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Assets.xcassets/AppIcon.appiconset/AppIcon29@2x-1.png differ diff --git a/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Assets.xcassets/AppIcon.appiconset/AppIcon29@2x.png b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Assets.xcassets/AppIcon.appiconset/AppIcon29@2x.png new file mode 100644 index 00000000..72064904 Binary files /dev/null and b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Assets.xcassets/AppIcon.appiconset/AppIcon29@2x.png differ diff --git a/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Assets.xcassets/AppIcon.appiconset/AppIcon29@3x.png b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Assets.xcassets/AppIcon.appiconset/AppIcon29@3x.png new file mode 100644 index 00000000..44d76708 Binary files /dev/null and b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Assets.xcassets/AppIcon.appiconset/AppIcon29@3x.png differ diff --git a/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Assets.xcassets/AppIcon.appiconset/AppIcon40@1x.png b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Assets.xcassets/AppIcon.appiconset/AppIcon40@1x.png new file mode 100644 index 00000000..6a8f8bed Binary files /dev/null and b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Assets.xcassets/AppIcon.appiconset/AppIcon40@1x.png differ diff --git a/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Assets.xcassets/AppIcon.appiconset/AppIcon40@2x-1.png b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Assets.xcassets/AppIcon.appiconset/AppIcon40@2x-1.png new file mode 100644 index 00000000..4e9810e8 Binary files /dev/null and b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Assets.xcassets/AppIcon.appiconset/AppIcon40@2x-1.png differ diff --git a/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Assets.xcassets/AppIcon.appiconset/AppIcon40@2x.png b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Assets.xcassets/AppIcon.appiconset/AppIcon40@2x.png new file mode 100644 index 00000000..4e9810e8 Binary files /dev/null and b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Assets.xcassets/AppIcon.appiconset/AppIcon40@2x.png differ diff --git a/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Assets.xcassets/AppIcon.appiconset/AppIcon40@3x.png b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Assets.xcassets/AppIcon.appiconset/AppIcon40@3x.png new file mode 100644 index 00000000..0d3d5742 Binary files /dev/null and b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Assets.xcassets/AppIcon.appiconset/AppIcon40@3x.png differ diff --git a/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Assets.xcassets/AppIcon.appiconset/AppIcon60@2x.png b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Assets.xcassets/AppIcon.appiconset/AppIcon60@2x.png new file mode 100644 index 00000000..9724b1cc Binary files /dev/null and b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Assets.xcassets/AppIcon.appiconset/AppIcon60@2x.png differ diff --git a/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Assets.xcassets/AppIcon.appiconset/AppIcon60@3x.png b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Assets.xcassets/AppIcon.appiconset/AppIcon60@3x.png new file mode 100644 index 00000000..4a25e378 Binary files /dev/null and b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Assets.xcassets/AppIcon.appiconset/AppIcon60@3x.png differ diff --git a/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Assets.xcassets/AppIcon.appiconset/AppIcon76@1x.png b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Assets.xcassets/AppIcon.appiconset/AppIcon76@1x.png new file mode 100644 index 00000000..f3f637e7 Binary files /dev/null and b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Assets.xcassets/AppIcon.appiconset/AppIcon76@1x.png differ diff --git a/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Assets.xcassets/AppIcon.appiconset/AppIcon76@2x.png b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Assets.xcassets/AppIcon.appiconset/AppIcon76@2x.png new file mode 100644 index 00000000..3391e7a0 Binary files /dev/null and b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Assets.xcassets/AppIcon.appiconset/AppIcon76@2x.png differ diff --git a/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Assets.xcassets/AppIcon.appiconset/AppIcon83.5@2x.png b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Assets.xcassets/AppIcon.appiconset/AppIcon83.5@2x.png new file mode 100644 index 00000000..5cbcab7f Binary files /dev/null and b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Assets.xcassets/AppIcon.appiconset/AppIcon83.5@2x.png differ diff --git a/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Assets.xcassets/AppIcon.appiconset/Contents.json b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..1facbcd6 --- /dev/null +++ b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,116 @@ +{ + "images" : [ + { + "filename" : "AppIcon20@2x-1.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "filename" : "AppIcon20@3x.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "filename" : "AppIcon29@2x-1.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "filename" : "AppIcon29@3x.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "filename" : "AppIcon40@2x-1.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "filename" : "AppIcon40@3x.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "filename" : "AppIcon60@2x.png", + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "filename" : "AppIcon60@3x.png", + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "filename" : "AppIcon20@1x.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "20x20" + }, + { + "filename" : "AppIcon20@2x.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "20x20" + }, + { + "filename" : "AppIcon29@1x.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "29x29" + }, + { + "filename" : "AppIcon29@2x.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "29x29" + }, + { + "filename" : "AppIcon40@1x.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "40x40" + }, + { + "filename" : "AppIcon40@2x.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "40x40" + }, + { + "filename" : "AppIcon76@1x.png", + "idiom" : "ipad", + "scale" : "1x", + "size" : "76x76" + }, + { + "filename" : "AppIcon76@2x.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "76x76" + }, + { + "filename" : "AppIcon83.5@2x.png", + "idiom" : "ipad", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "filename" : "AppIcon1024@1x.png", + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Assets.xcassets/Contents.json b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Assets.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Assets.xcassets/Themes/Contents.json b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Assets.xcassets/Themes/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Assets.xcassets/Themes/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Assets.xcassets/Themes/bubblegum.colorset/Contents.json b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Assets.xcassets/Themes/bubblegum.colorset/Contents.json new file mode 100644 index 00000000..849c4cbf --- /dev/null +++ b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Assets.xcassets/Themes/bubblegum.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.820", + "green" : "0.502", + "red" : "0.933" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.820", + "green" : "0.502", + "red" : "0.933" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Assets.xcassets/Themes/buttercup.colorset/Contents.json b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Assets.xcassets/Themes/buttercup.colorset/Contents.json new file mode 100644 index 00000000..92c0b5a8 --- /dev/null +++ b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Assets.xcassets/Themes/buttercup.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.588", + "green" : "0.945", + "red" : "1.000" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.588", + "green" : "0.945", + "red" : "1.000" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Assets.xcassets/Themes/indigo.colorset/Contents.json b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Assets.xcassets/Themes/indigo.colorset/Contents.json new file mode 100644 index 00000000..d9daea3e --- /dev/null +++ b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Assets.xcassets/Themes/indigo.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.443", + "green" : "0.000", + "red" : "0.212" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.443", + "green" : "0.000", + "red" : "0.212" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Assets.xcassets/Themes/lavender.colorset/Contents.json b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Assets.xcassets/Themes/lavender.colorset/Contents.json new file mode 100644 index 00000000..f95edce0 --- /dev/null +++ b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Assets.xcassets/Themes/lavender.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "0.808", + "red" : "0.812" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "0.808", + "red" : "0.812" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Assets.xcassets/Themes/magenta.colorset/Contents.json b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Assets.xcassets/Themes/magenta.colorset/Contents.json new file mode 100644 index 00000000..b20bdf59 --- /dev/null +++ b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Assets.xcassets/Themes/magenta.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.467", + "green" : "0.075", + "red" : "0.647" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.467", + "green" : "0.075", + "red" : "0.647" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Assets.xcassets/Themes/navy.colorset/Contents.json b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Assets.xcassets/Themes/navy.colorset/Contents.json new file mode 100644 index 00000000..821f22f7 --- /dev/null +++ b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Assets.xcassets/Themes/navy.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.255", + "green" : "0.078", + "red" : "0.000" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.255", + "green" : "0.078", + "red" : "0.000" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Assets.xcassets/Themes/orange.colorset/Contents.json b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Assets.xcassets/Themes/orange.colorset/Contents.json new file mode 100644 index 00000000..863c8c72 --- /dev/null +++ b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Assets.xcassets/Themes/orange.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.259", + "green" : "0.545", + "red" : "1.000" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.259", + "green" : "0.545", + "red" : "1.000" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Assets.xcassets/Themes/oxblood.colorset/Contents.json b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Assets.xcassets/Themes/oxblood.colorset/Contents.json new file mode 100644 index 00000000..0821af29 --- /dev/null +++ b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Assets.xcassets/Themes/oxblood.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.043", + "green" : "0.027", + "red" : "0.290" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.043", + "green" : "0.027", + "red" : "0.290" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Assets.xcassets/Themes/periwinkle.colorset/Contents.json b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Assets.xcassets/Themes/periwinkle.colorset/Contents.json new file mode 100644 index 00000000..8d29c91c --- /dev/null +++ b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Assets.xcassets/Themes/periwinkle.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "0.510", + "red" : "0.525" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "0.510", + "red" : "0.525" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Assets.xcassets/Themes/poppy.colorset/Contents.json b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Assets.xcassets/Themes/poppy.colorset/Contents.json new file mode 100644 index 00000000..d6a984fc --- /dev/null +++ b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Assets.xcassets/Themes/poppy.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.369", + "green" : "0.369", + "red" : "1.000" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.369", + "green" : "0.369", + "red" : "1.000" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Assets.xcassets/Themes/purple.colorset/Contents.json b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Assets.xcassets/Themes/purple.colorset/Contents.json new file mode 100644 index 00000000..b19089a1 --- /dev/null +++ b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Assets.xcassets/Themes/purple.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.949", + "green" : "0.294", + "red" : "0.569" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.949", + "green" : "0.294", + "red" : "0.569" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Assets.xcassets/Themes/seafoam.colorset/Contents.json b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Assets.xcassets/Themes/seafoam.colorset/Contents.json new file mode 100644 index 00000000..39065d2a --- /dev/null +++ b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Assets.xcassets/Themes/seafoam.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.898", + "green" : "0.918", + "red" : "0.796" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.898", + "green" : "0.918", + "red" : "0.796" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Assets.xcassets/Themes/sky.colorset/Contents.json b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Assets.xcassets/Themes/sky.colorset/Contents.json new file mode 100644 index 00000000..91e82482 --- /dev/null +++ b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Assets.xcassets/Themes/sky.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "0.573", + "red" : "0.431" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "0.573", + "red" : "0.431" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Assets.xcassets/Themes/tan.colorset/Contents.json b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Assets.xcassets/Themes/tan.colorset/Contents.json new file mode 100644 index 00000000..e42a6726 --- /dev/null +++ b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Assets.xcassets/Themes/tan.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.494", + "green" : "0.608", + "red" : "0.761" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.494", + "green" : "0.608", + "red" : "0.761" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Assets.xcassets/Themes/teal.colorset/Contents.json b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Assets.xcassets/Themes/teal.colorset/Contents.json new file mode 100644 index 00000000..a43d6577 --- /dev/null +++ b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Assets.xcassets/Themes/teal.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.620", + "green" : "0.561", + "red" : "0.133" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.620", + "green" : "0.561", + "red" : "0.133" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Assets.xcassets/Themes/yellow.colorset/Contents.json b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Assets.xcassets/Themes/yellow.colorset/Contents.json new file mode 100644 index 00000000..ce3b3be8 --- /dev/null +++ b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Assets.xcassets/Themes/yellow.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.302", + "green" : "0.875", + "red" : "1.000" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.302", + "green" : "0.875", + "red" : "1.000" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Models/AVPlayer+Ding.swift b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Models/AVPlayer+Ding.swift new file mode 100644 index 00000000..281ca2e3 --- /dev/null +++ b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Models/AVPlayer+Ding.swift @@ -0,0 +1,13 @@ +/* +See LICENSE folder for this sample’s licensing information. +*/ + +import Foundation +import AVFoundation + +extension AVPlayer { + static let sharedDingPlayer: AVPlayer = { + guard let url = Bundle.main.url(forResource: "ding", withExtension: "wav") else { fatalError("Failed to find sound file.") } + return AVPlayer(url: url) + }() +} diff --git a/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Models/DailyScrum.swift b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Models/DailyScrum.swift new file mode 100644 index 00000000..c710140e --- /dev/null +++ b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Models/DailyScrum.swift @@ -0,0 +1,64 @@ +/* + See LICENSE folder for this sample’s licensing information. + */ + +import Foundation + +struct DailyScrum: Identifiable, Codable { + let id: UUID + var title: String + var attendees: [Attendee] + var lengthInMinutes: Int + var lengthInMinutesAsDouble: Double { + get { + Double(lengthInMinutes) + } + set { + lengthInMinutes = Int(newValue) + } + } + var theme: Theme + var history: [History] = [] + + init(id: UUID = UUID(), title: String, attendees: [String], lengthInMinutes: Int, theme: Theme) { + self.id = id + self.title = title + self.attendees = attendees.map { Attendee(name: $0) } + self.lengthInMinutes = lengthInMinutes + self.theme = theme + } +} + +extension DailyScrum { + struct Attendee: Identifiable, Codable { + let id: UUID + var name: String + + init(id: UUID = UUID(), name: String) { + self.id = id + self.name = name + } + } + + static var emptyScrum: DailyScrum { + DailyScrum(title: "", attendees: [], lengthInMinutes: 5, theme: .sky) + } +} + +extension DailyScrum { + static let sampleData: [DailyScrum] = + [ + DailyScrum(title: "Design", + attendees: ["Cathy", "Daisy", "Simon", "Jonathan"], + lengthInMinutes: 10, + theme: .yellow), + DailyScrum(title: "App Dev", + attendees: ["Katie", "Gray", "Euna", "Luis", "Darla"], + lengthInMinutes: 5, + theme: .orange), + DailyScrum(title: "Web Dev", + attendees: ["Chella", "Chris", "Christina", "Eden", "Karla", "Lindsey", "Aga", "Chad", "Jenn", "Sarah"], + lengthInMinutes: 5, + theme: .poppy) + ] +} diff --git a/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Models/ErrorWrapper.swift b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Models/ErrorWrapper.swift new file mode 100644 index 00000000..438a8e29 --- /dev/null +++ b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Models/ErrorWrapper.swift @@ -0,0 +1,17 @@ +/* + See LICENSE folder for this sample’s licensing information. + */ + +import Foundation + +struct ErrorWrapper: Identifiable { + let id: UUID + let error: Error + let guidance: String + + init(id: UUID = UUID(), error: Error, guidance: String) { + self.id = id + self.error = error + self.guidance = guidance + } +} diff --git a/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Models/History.swift b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Models/History.swift new file mode 100644 index 00000000..2a228690 --- /dev/null +++ b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Models/History.swift @@ -0,0 +1,19 @@ +/* + See LICENSE folder for this sample’s licensing information. + */ + +import Foundation + +struct History: Identifiable, Codable { + let id: UUID + let date: Date + var attendees: [DailyScrum.Attendee] + var transcript: String? + + init(id: UUID = UUID(), date: Date = Date(), attendees: [DailyScrum.Attendee], transcript: String? = nil) { + self.id = id + self.date = date + self.attendees = attendees + self.transcript = transcript + } +} diff --git a/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Models/ScrumStore.swift b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Models/ScrumStore.swift new file mode 100644 index 00000000..317b5c8e --- /dev/null +++ b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Models/ScrumStore.swift @@ -0,0 +1,40 @@ +/* + See LICENSE folder for this sample’s licensing information. + */ + +import SwiftUI + +@MainActor +class ScrumStore: ObservableObject { + @Published var scrums: [DailyScrum] = [] + + private static func fileURL() throws -> URL { + try FileManager.default.url(for: .documentDirectory, + in: .userDomainMask, + appropriateFor: nil, + create: false) + .appendingPathComponent("scrums.data") + } + + func load() async throws { + let task = Task<[DailyScrum], Error> { + let fileURL = try Self.fileURL() + guard let data = try? Data(contentsOf: fileURL) else { + return [] + } + let dailyScrums = try JSONDecoder().decode([DailyScrum].self, from: data) + return dailyScrums + } + let scrums = try await task.value + self.scrums = scrums + } + + func save(scrums: [DailyScrum]) async throws { + let task = Task { + let data = try JSONEncoder().encode(scrums) + let outfile = try Self.fileURL() + try data.write(to: outfile) + } + _ = try await task.value + } +} diff --git a/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Models/ScrumTimer.swift b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Models/ScrumTimer.swift new file mode 100644 index 00000000..c78cb0bd --- /dev/null +++ b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Models/ScrumTimer.swift @@ -0,0 +1,145 @@ +/* +See LICENSE folder for this sample’s licensing information. +*/ + +import Foundation + +/// Keeps time for a daily scrum meeting. Keep track of the total meeting time, the time for each speaker, and the name of the current speaker. + +@MainActor +final class ScrumTimer: ObservableObject { + /// A struct to keep track of meeting attendees during a meeting. + struct Speaker: Identifiable { + /// The attendee name. + let name: String + /// True if the attendee has completed their turn to speak. + var isCompleted: Bool + /// Id for Identifiable conformance. + let id = UUID() + } + + /// The name of the meeting attendee who is speaking. + @Published var activeSpeaker = "" + /// The number of seconds since the beginning of the meeting. + @Published var secondsElapsed = 0 + /// The number of seconds until all attendees have had a turn to speak. + @Published var secondsRemaining = 0 + /// All meeting attendees, listed in the order they will speak. + private(set) var speakers: [Speaker] = [] + + /// The scrum meeting length. + private(set) var lengthInMinutes: Int + /// A closure that is executed when a new attendee begins speaking. + var speakerChangedAction: (() -> Void)? + + private weak var timer: Timer? + private var timerStopped = false + private var frequency: TimeInterval { 1.0 / 60.0 } + private var lengthInSeconds: Int { lengthInMinutes * 60 } + private var secondsPerSpeaker: Int { + (lengthInMinutes * 60) / speakers.count + } + private var secondsElapsedForSpeaker: Int = 0 + private var speakerIndex: Int = 0 + private var speakerText: String { + return "Speaker \(speakerIndex + 1): " + speakers[speakerIndex].name + } + private var startDate: Date? + + /** + Initialize a new timer. Initializing a time with no arguments creates a ScrumTimer with no attendees and zero length. + Use `startScrum()` to start the timer. + + - Parameters: + - lengthInMinutes: The meeting length. + - attendees: A list of attendees for the meeting. + */ + init(lengthInMinutes: Int = 0, attendees: [DailyScrum.Attendee] = []) { + self.lengthInMinutes = lengthInMinutes + self.speakers = attendees.speakers + secondsRemaining = lengthInSeconds + activeSpeaker = speakerText + } + + /// Start the timer. + func startScrum() { + timer = Timer.scheduledTimer(withTimeInterval: frequency, repeats: true) { [weak self] timer in + self?.update() + } + timer?.tolerance = 0.1 + changeToSpeaker(at: 0) + } + + /// Stop the timer. + func stopScrum() { + timer?.invalidate() + timerStopped = true + } + + /// Advance the timer to the next speaker. + nonisolated func skipSpeaker() { + Task { @MainActor in + changeToSpeaker(at: speakerIndex + 1) + } + } + + private func changeToSpeaker(at index: Int) { + if index > 0 { + let previousSpeakerIndex = index - 1 + speakers[previousSpeakerIndex].isCompleted = true + } + secondsElapsedForSpeaker = 0 + guard index < speakers.count else { return } + speakerIndex = index + activeSpeaker = speakerText + + secondsElapsed = index * secondsPerSpeaker + secondsRemaining = lengthInSeconds - secondsElapsed + startDate = Date() + } + + nonisolated private func update() { + + Task { @MainActor in + guard let startDate, + !timerStopped else { return } + let secondsElapsed = Int(Date().timeIntervalSince1970 - startDate.timeIntervalSince1970) + secondsElapsedForSpeaker = secondsElapsed + self.secondsElapsed = secondsPerSpeaker * speakerIndex + secondsElapsedForSpeaker + guard secondsElapsed <= secondsPerSpeaker else { + return + } + secondsRemaining = max(lengthInSeconds - self.secondsElapsed, 0) + + if secondsElapsedForSpeaker >= secondsPerSpeaker { + changeToSpeaker(at: speakerIndex + 1) + speakerChangedAction?() + } + } + } + + /** + Reset the timer with a new meeting length and new attendees. + + - Parameters: + - lengthInMinutes: The meeting length. + - attendees: The name of each attendee. + */ + func reset(lengthInMinutes: Int, attendees: [DailyScrum.Attendee]) { + self.lengthInMinutes = lengthInMinutes + self.speakers = attendees.speakers + secondsRemaining = lengthInSeconds + activeSpeaker = speakerText + } +} + + +extension Array { + var speakers: [ScrumTimer.Speaker] { + if isEmpty { + return [ScrumTimer.Speaker(name: "Speaker 1", isCompleted: false)] + } else { + return map { ScrumTimer.Speaker(name: $0.name, isCompleted: false) } + } + } +} diff --git a/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Models/SpeechRecognizer.swift b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Models/SpeechRecognizer.swift new file mode 100644 index 00000000..6e1da515 --- /dev/null +++ b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Models/SpeechRecognizer.swift @@ -0,0 +1,184 @@ +/* + See LICENSE folder for this sample’s licensing information. + */ + +import Foundation +import AVFoundation +import Speech +import SwiftUI + +/// A helper for transcribing speech to text using SFSpeechRecognizer and AVAudioEngine. +actor SpeechRecognizer: ObservableObject { + enum RecognizerError: Error { + case nilRecognizer + case notAuthorizedToRecognize + case notPermittedToRecord + case recognizerIsUnavailable + + var message: String { + switch self { + case .nilRecognizer: return "Can't initialize speech recognizer" + case .notAuthorizedToRecognize: return "Not authorized to recognize speech" + case .notPermittedToRecord: return "Not permitted to record audio" + case .recognizerIsUnavailable: return "Recognizer is unavailable" + } + } + } + + @MainActor var transcript: String = "" + + private var audioEngine: AVAudioEngine? + private var request: SFSpeechAudioBufferRecognitionRequest? + private var task: SFSpeechRecognitionTask? + private let recognizer: SFSpeechRecognizer? + + /** + Initializes a new speech recognizer. If this is the first time you've used the class, it + requests access to the speech recognizer and the microphone. + */ + init() { + recognizer = SFSpeechRecognizer() + guard recognizer != nil else { + transcribe(RecognizerError.nilRecognizer) + return + } + + Task { + do { + guard await SFSpeechRecognizer.hasAuthorizationToRecognize() else { + throw RecognizerError.notAuthorizedToRecognize + } + guard await AVAudioSession.sharedInstance().hasPermissionToRecord() else { + throw RecognizerError.notPermittedToRecord + } + } catch { + transcribe(error) + } + } + } + + @MainActor func startTranscribing() { + Task { + await transcribe() + } + } + + @MainActor func resetTranscript() { + Task { + await reset() + } + } + + @MainActor func stopTranscribing() { + Task { + await reset() + } + } + + /** + Begin transcribing audio. + + Creates a `SFSpeechRecognitionTask` that transcribes speech to text until you call `stopTranscribing()`. + The resulting transcription is continuously written to the published `transcript` property. + */ + private func transcribe() { + guard let recognizer, recognizer.isAvailable else { + self.transcribe(RecognizerError.recognizerIsUnavailable) + return + } + + do { + let (audioEngine, request) = try Self.prepareEngine() + self.audioEngine = audioEngine + self.request = request + self.task = recognizer.recognitionTask(with: request, resultHandler: { [weak self] result, error in + self?.recognitionHandler(audioEngine: audioEngine, result: result, error: error) + }) + } catch { + self.reset() + self.transcribe(error) + } + } + + /// Reset the speech recognizer. + private func reset() { + task?.cancel() + audioEngine?.stop() + audioEngine = nil + request = nil + task = nil + } + + private static func prepareEngine() throws -> (AVAudioEngine, SFSpeechAudioBufferRecognitionRequest) { + let audioEngine = AVAudioEngine() + + let request = SFSpeechAudioBufferRecognitionRequest() + request.shouldReportPartialResults = true + + let audioSession = AVAudioSession.sharedInstance() + try audioSession.setCategory(.playAndRecord, mode: .measurement, options: .duckOthers) + try audioSession.setActive(true, options: .notifyOthersOnDeactivation) + let inputNode = audioEngine.inputNode + + let recordingFormat = inputNode.outputFormat(forBus: 0) + inputNode.installTap(onBus: 0, bufferSize: 1024, format: recordingFormat) { (buffer: AVAudioPCMBuffer, when: AVAudioTime) in + request.append(buffer) + } + audioEngine.prepare() + try audioEngine.start() + + return (audioEngine, request) + } + + nonisolated private func recognitionHandler(audioEngine: AVAudioEngine, result: SFSpeechRecognitionResult?, error: Error?) { + let receivedFinalResult = result?.isFinal ?? false + let receivedError = error != nil + + if receivedFinalResult || receivedError { + audioEngine.stop() + audioEngine.inputNode.removeTap(onBus: 0) + } + + if let result { + transcribe(result.bestTranscription.formattedString) + } + } + + + nonisolated private func transcribe(_ message: String) { + Task { @MainActor in + transcript = message + } + } + nonisolated private func transcribe(_ error: Error) { + var errorMessage = "" + if let error = error as? RecognizerError { + errorMessage += error.message + } else { + errorMessage += error.localizedDescription + } + Task { @MainActor [errorMessage] in + transcript = "<< \(errorMessage) >>" + } + } +} + +extension SFSpeechRecognizer { + static func hasAuthorizationToRecognize() async -> Bool { + await withCheckedContinuation { continuation in + requestAuthorization { status in + continuation.resume(returning: status == .authorized) + } + } + } +} + +extension AVAudioSession { + func hasPermissionToRecord() async -> Bool { + await withCheckedContinuation { continuation in + requestRecordPermission { authorized in + continuation.resume(returning: authorized) + } + } + } +} diff --git a/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Models/Theme.swift b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Models/Theme.swift new file mode 100644 index 00000000..400dd844 --- /dev/null +++ b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Models/Theme.swift @@ -0,0 +1,41 @@ +/* + See LICENSE folder for this sample’s licensing information. + */ + +import SwiftUI + +enum Theme: String, CaseIterable, Identifiable, Codable { + + case bubblegum + case buttercup + case indigo + case lavender + case magenta + case navy + case orange + case oxblood + case periwinkle + case poppy + case purple + case seafoam + case sky + case tan + case teal + case yellow + + var accentColor: Color { + switch self { + case .bubblegum, .buttercup, .lavender, .orange, .periwinkle, .poppy, .seafoam, .sky, .tan, .teal, .yellow: return .black + case .indigo, .magenta, .navy, .oxblood, .purple: return .white + } + } + var mainColor: Color { + Color(rawValue) + } + var name: String { + rawValue.capitalized + } + var id: String { + name + } +} diff --git a/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Preview Content/Preview Assets.xcassets/Contents.json b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Resources/ding.wav b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Resources/ding.wav new file mode 100644 index 00000000..5831df26 Binary files /dev/null and b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Resources/ding.wav differ diff --git a/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/ScrumdingerApp.swift b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/ScrumdingerApp.swift new file mode 100644 index 00000000..101f8ae2 --- /dev/null +++ b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/ScrumdingerApp.swift @@ -0,0 +1,39 @@ +/* + See LICENSE folder for this sample’s licensing information. + */ + +import SwiftUI + +@main +struct ScrumdingerApp: App { + @StateObject private var store = ScrumStore() + @State private var errorWrapper: ErrorWrapper? + + var body: some Scene { + WindowGroup { + ScrumsView(scrums: $store.scrums) { + Task { + do { + try await store.save(scrums: store.scrums) + } catch { + errorWrapper = ErrorWrapper(error: error, + guidance: "Try again later.") + } + } + } + .task { + do { + try await store.load() + } catch { + errorWrapper = ErrorWrapper(error: error, + guidance: "Scrumdinger will load sample data and continue.") + } + } + .sheet(item: $errorWrapper) { + store.scrums = DailyScrum.sampleData + } content: { wrapper in + ErrorView(errorWrapper: wrapper) + } + } + } +} diff --git a/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Views/CardView.swift b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Views/CardView.swift new file mode 100644 index 00000000..9dadd168 --- /dev/null +++ b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Views/CardView.swift @@ -0,0 +1,37 @@ +/* + See LICENSE folder for this sample’s licensing information. + */ + +import SwiftUI + +struct CardView: View { + let scrum: DailyScrum + var body: some View { + VStack(alignment: .leading) { + Text(scrum.title) + .font(.headline) + .accessibilityAddTraits(.isHeader) + Spacer() + HStack { + Label("\(scrum.attendees.count)", systemImage: "person.3") + .accessibilityLabel("\(scrum.attendees.count) attendees") + Spacer() + Label("\(scrum.lengthInMinutes)", systemImage: "clock") + .accessibilityLabel("\(scrum.lengthInMinutes) minute meeting") + .labelStyle(.trailingIcon) + } + .font(.caption) + } + .padding() + .foregroundColor(scrum.theme.accentColor) + } +} + +struct CardView_Previews: PreviewProvider { + static var scrum = DailyScrum.sampleData[0] + static var previews: some View { + CardView(scrum: scrum) + .background(scrum.theme.mainColor) + .previewLayout(.fixed(width: 400, height: 60)) + } +} diff --git a/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Views/DetailEditView.swift b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Views/DetailEditView.swift new file mode 100644 index 00000000..34461c11 --- /dev/null +++ b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Views/DetailEditView.swift @@ -0,0 +1,56 @@ +/* + See LICENSE folder for this sample’s licensing information. + */ + +import SwiftUI + +struct DetailEditView: View { + @Binding var scrum: DailyScrum + @State private var newAttendeeName = "" + + var body: some View { + Form { + Section(header: Text("Meeting Info")) { + TextField("Title", text: $scrum.title) + HStack { + Slider(value: $scrum.lengthInMinutesAsDouble, in: 2...30, step: 1) { + Text("Length") + } + .accessibilityValue("\(scrum.lengthInMinutes) minutes") + Spacer() + Text("\(scrum.lengthInMinutes) minutes") + .accessibilityHidden(true) + } + ThemePicker(selection: $scrum.theme) + } + Section(header: Text("Attendees")) { + ForEach(scrum.attendees) { attendee in + Text(attendee.name) + } + .onDelete { indices in + scrum.attendees.remove(atOffsets: indices) + } + HStack { + TextField("New Attendee", text: $newAttendeeName) + Button(action: { + withAnimation { + let attendee = DailyScrum.Attendee(name: newAttendeeName) + scrum.attendees.append(attendee) + newAttendeeName = "" + } + }) { + Image(systemName: "plus.circle.fill") + .accessibilityLabel("Add attendee") + } + .disabled(newAttendeeName.isEmpty) + } + } + } + } +} + +struct DetailEditView_Previews: PreviewProvider { + static var previews: some View { + DetailEditView(scrum: .constant(DailyScrum.sampleData[0])) + } +} diff --git a/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Views/DetailView.swift b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Views/DetailView.swift new file mode 100644 index 00000000..c8117930 --- /dev/null +++ b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Views/DetailView.swift @@ -0,0 +1,91 @@ +/* + See LICENSE folder for this sample’s licensing information. + */ + +import SwiftUI + +struct DetailView: View { + @Binding var scrum: DailyScrum + @State private var editingScrum = DailyScrum.emptyScrum + @State private var isPresentingEditView = false + + var body: some View { + List { + Section(header: Text("Meeting Info")) { + NavigationLink(destination: MeetingView(scrum: $scrum)) { + Label("Start Meeting", systemImage: "timer") + .font(.headline) + .foregroundColor(.accentColor) + } + HStack { + Label("Length", systemImage: "clock") + Spacer() + Text("\(scrum.lengthInMinutes) minutes") + } + .accessibilityElement(children: .combine) + HStack { + Label("Theme", systemImage: "paintpalette") + Spacer() + Text(scrum.theme.name) + .padding(4) + .foregroundColor(scrum.theme.accentColor) + .background(scrum.theme.mainColor) + .cornerRadius(4) + } + .accessibilityElement(children: .combine) + } + Section(header: Text("Attendees")) { + ForEach(scrum.attendees) { attendee in + Label(attendee.name, systemImage: "person") + } + } + Section(header: Text("History")) { + if scrum.history.isEmpty { + Label("No meetings yet", systemImage: "calendar.badge.exclamationmark") + } + ForEach(scrum.history) { history in + NavigationLink(destination: HistoryView(history: history)) { + HStack { + Image(systemName: "calendar") + Text(history.date, style: .date) + } + } + } + } + } + .navigationTitle(scrum.title) + .toolbar { + Button("Edit") { + isPresentingEditView = true + editingScrum = scrum + } + } + .sheet(isPresented: $isPresentingEditView) { + NavigationStack { + DetailEditView(scrum: $editingScrum) + .navigationTitle(scrum.title) + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button("Cancel") { + isPresentingEditView = false + } + } + ToolbarItem(placement: .confirmationAction) { + Button("Done") { + isPresentingEditView = false + scrum = editingScrum + } + } + } + } + } + } +} + +struct DetailView_Previews: PreviewProvider { + static var previews: some View { + NavigationStack { + DetailView(scrum: .constant(DailyScrum.sampleData[0])) + } + } +} diff --git a/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Views/ErrorView.swift b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Views/ErrorView.swift new file mode 100644 index 00000000..7af03401 --- /dev/null +++ b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Views/ErrorView.swift @@ -0,0 +1,51 @@ +/* + See LICENSE folder for this sample’s licensing information. + */ + +import SwiftUI + +struct ErrorView: View { + let errorWrapper: ErrorWrapper + @Environment(\.dismiss) private var dismiss + + var body: some View { + NavigationStack { + VStack { + Text("An error has occurred!") + .font(.title) + .padding(.bottom) + Text(errorWrapper.error.localizedDescription) + .font(.headline) + Text(errorWrapper.guidance) + .font(.caption) + .padding(.top) + Spacer() + } + .padding() + .background(.ultraThinMaterial) + .cornerRadius(16) + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + Button("Dismiss") { + dismiss() + } + } + } + } + } +} + +struct ErrorView_Previews: PreviewProvider { + enum SampleError: Error { + case errorRequired + } + + static var wrapper: ErrorWrapper { + ErrorWrapper(error: SampleError.errorRequired, + guidance: "You can safely ignore this error.") + } + + static var previews: some View { + ErrorView(errorWrapper: wrapper) + } +} diff --git a/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Views/HistoryView.swift b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Views/HistoryView.swift new file mode 100644 index 00000000..887c050b --- /dev/null +++ b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Views/HistoryView.swift @@ -0,0 +1,50 @@ +/* + See LICENSE folder for this sample’s licensing information. + */ + +import SwiftUI + +struct HistoryView: View { + let history: History + + var body: some View { + ScrollView { + VStack(alignment: .leading) { + Divider() + .padding(.bottom) + Text("Attendees") + .font(.headline) + Text(history.attendeeString) + if let transcript = history.transcript { + Text("Transcript") + .font(.headline) + .padding(.top) + Text(transcript) + } + } + } + .navigationTitle(Text(history.date, style: .date)) + .padding() + } +} + +extension History { + var attendeeString: String { + ListFormatter.localizedString(byJoining: attendees.map { $0.name }) + } +} + +struct HistoryView_Previews: PreviewProvider { + static var history: History { + History(attendees: [ + DailyScrum.Attendee(name: "Jon"), + DailyScrum.Attendee(name: "Darla"), + DailyScrum.Attendee(name: "Luis") + ], + transcript: "Darla, would you like to start today? Sure, yesterday I reviewed Luis' PR and met with the design team to finalize the UI...") + } + + static var previews: some View { + HistoryView(history: history) + } +} diff --git a/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Views/MeetingFooterView.swift b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Views/MeetingFooterView.swift new file mode 100644 index 00000000..9bbbce82 --- /dev/null +++ b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Views/MeetingFooterView.swift @@ -0,0 +1,47 @@ +/* + See LICENSE folder for this sample’s licensing information. + */ + +import SwiftUI + +struct MeetingFooterView: View { + let speakers: [ScrumTimer.Speaker] + var skipAction: ()->Void + + private var speakerNumber: Int? { + guard let index = speakers.firstIndex(where: { !$0.isCompleted }) else { return nil } + return index + 1 + } + private var isLastSpeaker: Bool { + return speakers.dropLast().allSatisfy { $0.isCompleted } + } + private var speakerText: String { + guard let speakerNumber = speakerNumber else { return "No more speakers" } + return "Speaker \(speakerNumber) of \(speakers.count)" + } + + var body: some View { + VStack { + HStack { + if isLastSpeaker { + Text("Last Speaker") + } else { + Text(speakerText) + Spacer() + Button(action: skipAction) { + Image(systemName: "forward.fill") + } + .accessibilityLabel("Next speaker") + } + } + } + .padding([.bottom, .horizontal]) + } +} + +struct MeetingFooterView_Previews: PreviewProvider { + static var previews: some View { + MeetingFooterView(speakers: DailyScrum.sampleData[0].attendees.speakers, skipAction: {}) + .previewLayout(.sizeThatFits) + } +} diff --git a/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Views/MeetingHeaderView.swift b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Views/MeetingHeaderView.swift new file mode 100644 index 00000000..21feb176 --- /dev/null +++ b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Views/MeetingHeaderView.swift @@ -0,0 +1,54 @@ +/* + See LICENSE folder for this sample’s licensing information. + */ + +import SwiftUI + +struct MeetingHeaderView: View { + let secondsElapsed: Int + let secondsRemaining: Int + let theme: Theme + + private var totalSeconds: Int { + secondsElapsed + secondsRemaining + } + private var progress: Double { + guard totalSeconds > 0 else { return 1 } + return Double(secondsElapsed) / Double(totalSeconds) + } + private var minutesRemaining: Int { + secondsRemaining / 60 + } + + var body: some View { + VStack { + ProgressView(value: progress) + .progressViewStyle(ScrumProgressViewStyle(theme: theme)) + HStack { + VStack(alignment: .leading) { + Text("Seconds Elapsed") + .font(.caption) + Label("\(secondsElapsed)", systemImage: "hourglass.tophalf.fill") + } + Spacer() + VStack(alignment: .trailing) { + Text("Seconds Remaining") + .font(.caption) + Label("\(secondsRemaining)", systemImage: "hourglass.bottomhalf.fill") + .labelStyle(.trailingIcon) + } + } + } + .accessibilityElement(children: .ignore) + .accessibilityLabel("Time remaining") + .accessibilityValue("\(minutesRemaining) minutes") + .padding([.top, .horizontal]) + } +} + +struct MeetingHeaderView_Previews: PreviewProvider { + static var previews: some View { + MeetingHeaderView(secondsElapsed: 60, secondsRemaining: 180, theme: .bubblegum) + .previewLayout(.sizeThatFits) + } +} diff --git a/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Views/MeetingTimerView.swift b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Views/MeetingTimerView.swift new file mode 100644 index 00000000..ad3df6ed --- /dev/null +++ b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Views/MeetingTimerView.swift @@ -0,0 +1,53 @@ +/* + See LICENSE folder for this sample’s licensing information. + */ + +import SwiftUI + +struct MeetingTimerView: View { + let speakers: [ScrumTimer.Speaker] + let isRecording: Bool + let theme: Theme + + private var currentSpeaker: String { + speakers.first(where: { !$0.isCompleted })?.name ?? "Someone" + } + + var body: some View { + Circle() + .strokeBorder(lineWidth: 24) + .overlay { + VStack { + Text(currentSpeaker) + .font(.title) + Text("is speaking") + Image(systemName: isRecording ? "mic" : "mic.slash") + .font(.title) + .padding(.top) + .accessibilityLabel(isRecording ? "with transcription" : "without transcription") + } + .accessibilityElement(children: .combine) + .foregroundStyle(theme.accentColor) + } + .overlay { + ForEach(speakers) { speaker in + if speaker.isCompleted, let index = speakers.firstIndex(where: { $0.id == speaker.id }) { + SpeakerArc(speakerIndex: index, totalSpeakers: speakers.count) + .rotation(Angle(degrees: -90)) + .stroke(theme.mainColor, lineWidth: 12) + } + } + } + .padding(.horizontal) + } +} + +struct MeetingTimerView_Previews: PreviewProvider { + static var speakers: [ScrumTimer.Speaker] { + [ScrumTimer.Speaker(name: "Bill", isCompleted: true), ScrumTimer.Speaker(name: "Cathy", isCompleted: false)] + } + + static var previews: some View { + MeetingTimerView(speakers: speakers, isRecording: true, theme: .yellow) + } +} diff --git a/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Views/MeetingView.swift b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Views/MeetingView.swift new file mode 100644 index 00000000..7c2b7625 --- /dev/null +++ b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Views/MeetingView.swift @@ -0,0 +1,63 @@ +/* + See LICENSE folder for this sample’s licensing information. + */ + +import SwiftUI +import AVFoundation + +struct MeetingView: View { + @Binding var scrum: DailyScrum + @StateObject var scrumTimer = ScrumTimer() + @StateObject var speechRecognizer = SpeechRecognizer() + @State private var isRecording = false + + private var player: AVPlayer { AVPlayer.sharedDingPlayer } + + var body: some View { + ZStack { + RoundedRectangle(cornerRadius: 16.0) + .fill(scrum.theme.mainColor) + VStack { + MeetingHeaderView(secondsElapsed: scrumTimer.secondsElapsed, secondsRemaining: scrumTimer.secondsRemaining, theme: scrum.theme) + MeetingTimerView(speakers: scrumTimer.speakers, isRecording: isRecording, theme: scrum.theme) + MeetingFooterView(speakers: scrumTimer.speakers, skipAction: scrumTimer.skipSpeaker) + } + } + .padding() + .foregroundColor(scrum.theme.accentColor) + .onAppear { + startScrum() + } + .onDisappear { + endScrum() + } + .navigationBarTitleDisplayMode(.inline) + } + + private func startScrum() { + scrumTimer.reset(lengthInMinutes: scrum.lengthInMinutes, attendees: scrum.attendees) + scrumTimer.speakerChangedAction = { + player.seek(to: .zero) + player.play() + } + speechRecognizer.resetTranscript() + speechRecognizer.startTranscribing() + isRecording = true + scrumTimer.startScrum() + } + + private func endScrum() { + scrumTimer.stopScrum() + speechRecognizer.stopTranscribing() + isRecording = false + let newHistory = History(attendees: scrum.attendees, + transcript: speechRecognizer.transcript) + scrum.history.insert(newHistory, at: 0) + } +} + +struct MeetingView_Previews: PreviewProvider { + static var previews: some View { + MeetingView(scrum: .constant(DailyScrum.sampleData[0])) + } +} diff --git a/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Views/NewScrumSheet.swift b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Views/NewScrumSheet.swift new file mode 100644 index 00000000..e8b87362 --- /dev/null +++ b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Views/NewScrumSheet.swift @@ -0,0 +1,36 @@ +/* + See LICENSE folder for this sample’s licensing information. + */ + +import SwiftUI + +struct NewScrumSheet: View { + @State private var newScrum = DailyScrum.emptyScrum + @Binding var scrums: [DailyScrum] + @Binding var isPresentingNewScrumView: Bool + + var body: some View { + NavigationStack { + DetailEditView(scrum: $newScrum) + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button("Dismiss") { + isPresentingNewScrumView = false + } + } + ToolbarItem(placement: .confirmationAction) { + Button("Add") { + scrums.append(newScrum) + isPresentingNewScrumView = false + } + } + } + } + } +} + +struct NewScrumSheet_Previews: PreviewProvider { + static var previews: some View { + NewScrumSheet(scrums: .constant(DailyScrum.sampleData), isPresentingNewScrumView: .constant(true)) + } +} diff --git a/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Views/ScrumProgressViewStyle.swift b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Views/ScrumProgressViewStyle.swift new file mode 100644 index 00000000..01820571 --- /dev/null +++ b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Views/ScrumProgressViewStyle.swift @@ -0,0 +1,35 @@ +/* +See LICENSE folder for this sample’s licensing information. +*/ + +import SwiftUI + +struct ScrumProgressViewStyle: ProgressViewStyle { + var theme: Theme + + func makeBody(configuration: Configuration) -> some View { + ZStack { + RoundedRectangle(cornerRadius: 10.0) + .fill(theme.accentColor) + .frame(height: 20.0) + if #available(iOS 15.0, *) { + ProgressView(configuration) + .tint(theme.mainColor) + .frame(height: 12.0) + .padding(.horizontal) + } else { + ProgressView(configuration) + .frame(height: 12.0) + .padding(.horizontal) + } + } + } +} + +struct ScrumProgressViewStyle_Previews: PreviewProvider { + static var previews: some View { + ProgressView(value: 0.4) + .progressViewStyle(ScrumProgressViewStyle(theme: .buttercup)) + .previewLayout(.sizeThatFits) + } +} diff --git a/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Views/ScrumsView.swift b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Views/ScrumsView.swift new file mode 100644 index 00000000..88f25be9 --- /dev/null +++ b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Views/ScrumsView.swift @@ -0,0 +1,44 @@ +/* + See LICENSE folder for this sample’s licensing information. + */ + +import SwiftUI + +struct ScrumsView: View { + @Binding var scrums: [DailyScrum] + @Environment(\.scenePhase) private var scenePhase + @State private var isPresentingNewScrumView = false + let saveAction: ()->Void + + var body: some View { + NavigationStack { + List($scrums) { $scrum in + NavigationLink(destination: DetailView(scrum: $scrum)) { + CardView(scrum: scrum) + } + .listRowBackground(scrum.theme.mainColor) + } + .navigationTitle("Daily Scrums") + .toolbar { + Button(action: { + isPresentingNewScrumView = true + }) { + Image(systemName: "plus") + } + .accessibilityLabel("New Scrum") + } + } + .sheet(isPresented: $isPresentingNewScrumView) { + NewScrumSheet(scrums: $scrums, isPresentingNewScrumView: $isPresentingNewScrumView) + } + .onChange(of: scenePhase) { phase in + if phase == .inactive { saveAction() } + } + } +} + +struct ScrumsView_Previews: PreviewProvider { + static var previews: some View { + ScrumsView(scrums: .constant(DailyScrum.sampleData), saveAction: {}) + } +} diff --git a/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Views/SpeakerArc.swift b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Views/SpeakerArc.swift new file mode 100644 index 00000000..2ea429ab --- /dev/null +++ b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Views/SpeakerArc.swift @@ -0,0 +1,29 @@ +/* + See LICENSE folder for this sample’s licensing information. + */ + +import SwiftUI + +struct SpeakerArc: Shape { + let speakerIndex: Int + let totalSpeakers: Int + + private var degreesPerSpeaker: Double { + 360.0 / Double(totalSpeakers) + } + private var startAngle: Angle { + Angle(degrees: degreesPerSpeaker * Double(speakerIndex) + 1.0) + } + private var endAngle: Angle { + Angle(degrees: startAngle.degrees + degreesPerSpeaker - 1.0) + } + + func path(in rect: CGRect) -> Path { + let diameter = min(rect.size.width, rect.size.height) - 24.0 + let radius = diameter / 2.0 + let center = CGPoint(x: rect.midX, y: rect.midY) + return Path { path in + path.addArc(center: center, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: false) + } + } +} diff --git a/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Views/ThemePicker.swift b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Views/ThemePicker.swift new file mode 100644 index 00000000..2e1ecfa9 --- /dev/null +++ b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Views/ThemePicker.swift @@ -0,0 +1,25 @@ +/* + See LICENSE folder for this sample’s licensing information. + */ + +import SwiftUI + +struct ThemePicker: View { + @Binding var selection: Theme + + var body: some View { + Picker("Theme", selection: $selection) { + ForEach(Theme.allCases) { theme in + ThemeView(theme: theme) + .tag(theme) + } + } + .pickerStyle(.navigationLink) + } +} + +struct ThemePicker_Previews: PreviewProvider { + static var previews: some View { + ThemePicker(selection: .constant(.periwinkle)) + } +} diff --git a/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Views/ThemeView.swift b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Views/ThemeView.swift new file mode 100644 index 00000000..5283d462 --- /dev/null +++ b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Views/ThemeView.swift @@ -0,0 +1,24 @@ +/* + See LICENSE folder for this sample’s licensing information. + */ + +import SwiftUI + +struct ThemeView: View { + let theme: Theme + + var body: some View { + Text(theme.name) + .padding(4) + .frame(maxWidth: .infinity) + .background(theme.mainColor) + .foregroundColor(theme.accentColor) + .clipShape(RoundedRectangle(cornerRadius: 4)) + } +} + +struct ThemeView_Previews: PreviewProvider { + static var previews: some View { + ThemeView(theme: .buttercup) + } +} diff --git a/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Views/TrailingIconLabelStyle.swift b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Views/TrailingIconLabelStyle.swift new file mode 100644 index 00000000..e4b5e110 --- /dev/null +++ b/0247-tca-tour-pt5/Scrumdinger-Complete/Scrumdinger/Views/TrailingIconLabelStyle.swift @@ -0,0 +1,18 @@ +/* + See LICENSE folder for this sample’s licensing information. + */ + +import SwiftUI + +struct TrailingIconLabelStyle: LabelStyle { + func makeBody(configuration: Configuration) -> some View { + HStack { + configuration.title + configuration.icon + } + } +} + +extension LabelStyle where Self == TrailingIconLabelStyle { + static var trailingIcon: Self { Self() } +} diff --git a/0247-tca-tour-pt5/Standups/Standups.xcodeproj/project.pbxproj b/0247-tca-tour-pt5/Standups/Standups.xcodeproj/project.pbxproj new file mode 100644 index 00000000..a6bf0a10 --- /dev/null +++ b/0247-tca-tour-pt5/Standups/Standups.xcodeproj/project.pbxproj @@ -0,0 +1,536 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 56; + objects = { + +/* Begin PBXBuildFile section */ + 2A4D5DCB2A69E97E0098984B /* StandupsListTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A4D5DCA2A69E97E0098984B /* StandupsListTests.swift */; }; + 2AB309192A6AF37C00FCC600 /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AB309182A6AF37C00FCC600 /* App.swift */; }; + 2AED4B442A69D0280099BFE2 /* StandupForm.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AED4B432A69D0280099BFE2 /* StandupForm.swift */; }; + 4B2536EB2A69CB1600C012CC /* StandupsList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B2536EA2A69CB1600C012CC /* StandupsList.swift */; }; + 4B2536ED2A69CBB500C012CC /* Models.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B2536EC2A69CBB500C012CC /* Models.swift */; }; + 4B2536EF2A69DB6400C012CC /* StandupFormTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B2536EE2A69DB6400C012CC /* StandupFormTests.swift */; }; + 4B600A412A6AEB97002B665B /* StandupDetail.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B600A402A6AEB97002B665B /* StandupDetail.swift */; }; + 4B600A432A6AF0D2002B665B /* StandupDetailTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B600A422A6AF0D2002B665B /* StandupDetailTests.swift */; }; + 4BBA95892A6AFEC100301693 /* AppTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BBA95882A6AFEC100301693 /* AppTests.swift */; }; + 4BBA958B2A6B129400301693 /* RecordMeeting.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4BBA958A2A6B129400301693 /* RecordMeeting.swift */; }; + CA6D66522A68263900B2A77A /* ComposableArchitecture in Frameworks */ = {isa = PBXBuildFile; productRef = CA6D66512A68263900B2A77A /* ComposableArchitecture */; }; + CA9CB7282A411ECD003BDB3B /* StandupsApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA9CB7272A411ECD003BDB3B /* StandupsApp.swift */; }; + CA9CB72C2A411ECD003BDB3B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = CA9CB72B2A411ECD003BDB3B /* Assets.xcassets */; }; + CA9CB72F2A411ECD003BDB3B /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = CA9CB72E2A411ECD003BDB3B /* Preview Assets.xcassets */; }; + CA9CB7392A411ECD003BDB3B /* StandupsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA9CB7382A411ECD003BDB3B /* StandupsTests.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + CA9CB7352A411ECD003BDB3B /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = CA9CB71C2A411ECC003BDB3B /* Project object */; + proxyType = 1; + remoteGlobalIDString = CA9CB7232A411ECD003BDB3B; + remoteInfo = Standups; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 2A4D5DCA2A69E97E0098984B /* StandupsListTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StandupsListTests.swift; sourceTree = ""; }; + 2AB309182A6AF37C00FCC600 /* App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App.swift; sourceTree = ""; }; + 2AED4B432A69D0280099BFE2 /* StandupForm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StandupForm.swift; sourceTree = ""; }; + 4B2536EA2A69CB1600C012CC /* StandupsList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StandupsList.swift; sourceTree = ""; }; + 4B2536EC2A69CBB500C012CC /* Models.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Models.swift; sourceTree = ""; }; + 4B2536EE2A69DB6400C012CC /* StandupFormTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StandupFormTests.swift; sourceTree = ""; }; + 4B600A402A6AEB97002B665B /* StandupDetail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StandupDetail.swift; sourceTree = ""; }; + 4B600A422A6AF0D2002B665B /* StandupDetailTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StandupDetailTests.swift; sourceTree = ""; }; + 4BBA95882A6AFEC100301693 /* AppTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppTests.swift; sourceTree = ""; }; + 4BBA958A2A6B129400301693 /* RecordMeeting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecordMeeting.swift; sourceTree = ""; }; + 4BBA958C2A6B189200301693 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = ""; }; + CA9CB7242A411ECD003BDB3B /* Standups.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Standups.app; sourceTree = BUILT_PRODUCTS_DIR; }; + CA9CB7272A411ECD003BDB3B /* StandupsApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StandupsApp.swift; sourceTree = ""; }; + CA9CB72B2A411ECD003BDB3B /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + CA9CB72E2A411ECD003BDB3B /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; + CA9CB7342A411ECD003BDB3B /* StandupsTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = StandupsTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + CA9CB7382A411ECD003BDB3B /* StandupsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StandupsTests.swift; sourceTree = ""; }; + CA9CB7512A411EF4003BDB3B /* Standups.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = Standups.xctestplan; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + CA9CB7212A411ECD003BDB3B /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + CA6D66522A68263900B2A77A /* ComposableArchitecture in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + CA9CB7312A411ECD003BDB3B /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + CA9CB71B2A411ECC003BDB3B = { + isa = PBXGroup; + children = ( + CA9CB7262A411ECD003BDB3B /* Standups */, + CA9CB7372A411ECD003BDB3B /* StandupsTests */, + CA9CB7252A411ECD003BDB3B /* Products */, + ); + sourceTree = ""; + }; + CA9CB7252A411ECD003BDB3B /* Products */ = { + isa = PBXGroup; + children = ( + CA9CB7242A411ECD003BDB3B /* Standups.app */, + CA9CB7342A411ECD003BDB3B /* StandupsTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + CA9CB7262A411ECD003BDB3B /* Standups */ = { + isa = PBXGroup; + children = ( + 4BBA958C2A6B189200301693 /* Info.plist */, + 2AB309182A6AF37C00FCC600 /* App.swift */, + 4B2536EC2A69CBB500C012CC /* Models.swift */, + 4BBA958A2A6B129400301693 /* RecordMeeting.swift */, + 4B600A402A6AEB97002B665B /* StandupDetail.swift */, + 2AED4B432A69D0280099BFE2 /* StandupForm.swift */, + CA9CB7272A411ECD003BDB3B /* StandupsApp.swift */, + 4B2536EA2A69CB1600C012CC /* StandupsList.swift */, + CA9CB72B2A411ECD003BDB3B /* Assets.xcassets */, + CA9CB72D2A411ECD003BDB3B /* Preview Content */, + ); + path = Standups; + sourceTree = ""; + }; + CA9CB72D2A411ECD003BDB3B /* Preview Content */ = { + isa = PBXGroup; + children = ( + CA9CB72E2A411ECD003BDB3B /* Preview Assets.xcassets */, + ); + path = "Preview Content"; + sourceTree = ""; + }; + CA9CB7372A411ECD003BDB3B /* StandupsTests */ = { + isa = PBXGroup; + children = ( + 4BBA95882A6AFEC100301693 /* AppTests.swift */, + 4B600A422A6AF0D2002B665B /* StandupDetailTests.swift */, + 4B2536EE2A69DB6400C012CC /* StandupFormTests.swift */, + 2A4D5DCA2A69E97E0098984B /* StandupsListTests.swift */, + CA9CB7382A411ECD003BDB3B /* StandupsTests.swift */, + CA9CB7512A411EF4003BDB3B /* Standups.xctestplan */, + ); + path = StandupsTests; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + CA9CB7232A411ECD003BDB3B /* Standups */ = { + isa = PBXNativeTarget; + buildConfigurationList = CA9CB7482A411ECD003BDB3B /* Build configuration list for PBXNativeTarget "Standups" */; + buildPhases = ( + CA9CB7202A411ECD003BDB3B /* Sources */, + CA9CB7212A411ECD003BDB3B /* Frameworks */, + CA9CB7222A411ECD003BDB3B /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Standups; + packageProductDependencies = ( + CA6D66512A68263900B2A77A /* ComposableArchitecture */, + ); + productName = Standups; + productReference = CA9CB7242A411ECD003BDB3B /* Standups.app */; + productType = "com.apple.product-type.application"; + }; + CA9CB7332A411ECD003BDB3B /* StandupsTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = CA9CB74B2A411ECD003BDB3B /* Build configuration list for PBXNativeTarget "StandupsTests" */; + buildPhases = ( + CA9CB7302A411ECD003BDB3B /* Sources */, + CA9CB7312A411ECD003BDB3B /* Frameworks */, + CA9CB7322A411ECD003BDB3B /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + CA9CB7362A411ECD003BDB3B /* PBXTargetDependency */, + ); + name = StandupsTests; + productName = StandupsTests; + productReference = CA9CB7342A411ECD003BDB3B /* StandupsTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + CA9CB71C2A411ECC003BDB3B /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1430; + LastUpgradeCheck = 1430; + TargetAttributes = { + CA9CB7232A411ECD003BDB3B = { + CreatedOnToolsVersion = 14.3.1; + }; + CA9CB7332A411ECD003BDB3B = { + CreatedOnToolsVersion = 14.3.1; + TestTargetID = CA9CB7232A411ECD003BDB3B; + }; + }; + }; + buildConfigurationList = CA9CB71F2A411ECC003BDB3B /* Build configuration list for PBXProject "Standups" */; + compatibilityVersion = "Xcode 14.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = CA9CB71B2A411ECC003BDB3B; + packageReferences = ( + CA6D66502A68263900B2A77A /* XCRemoteSwiftPackageReference "swift-composable-architecture" */, + ); + productRefGroup = CA9CB7252A411ECD003BDB3B /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + CA9CB7232A411ECD003BDB3B /* Standups */, + CA9CB7332A411ECD003BDB3B /* StandupsTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + CA9CB7222A411ECD003BDB3B /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + CA9CB72F2A411ECD003BDB3B /* Preview Assets.xcassets in Resources */, + CA9CB72C2A411ECD003BDB3B /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + CA9CB7322A411ECD003BDB3B /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + CA9CB7202A411ECD003BDB3B /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 2AED4B442A69D0280099BFE2 /* StandupForm.swift in Sources */, + 4B600A412A6AEB97002B665B /* StandupDetail.swift in Sources */, + 4B2536EB2A69CB1600C012CC /* StandupsList.swift in Sources */, + 2AB309192A6AF37C00FCC600 /* App.swift in Sources */, + 4BBA958B2A6B129400301693 /* RecordMeeting.swift in Sources */, + CA9CB7282A411ECD003BDB3B /* StandupsApp.swift in Sources */, + 4B2536ED2A69CBB500C012CC /* Models.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + CA9CB7302A411ECD003BDB3B /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + CA9CB7392A411ECD003BDB3B /* StandupsTests.swift in Sources */, + 2A4D5DCB2A69E97E0098984B /* StandupsListTests.swift in Sources */, + 4B2536EF2A69DB6400C012CC /* StandupFormTests.swift in Sources */, + 4BBA95892A6AFEC100301693 /* AppTests.swift in Sources */, + 4B600A432A6AF0D2002B665B /* StandupDetailTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + CA9CB7362A411ECD003BDB3B /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = CA9CB7232A411ECD003BDB3B /* Standups */; + targetProxy = CA9CB7352A411ECD003BDB3B /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + CA9CB7462A411ECD003BDB3B /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_STRICT_CONCURRENCY = complete; + }; + name = Debug; + }; + CA9CB7472A411ECD003BDB3B /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 17.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + SWIFT_STRICT_CONCURRENCY = complete; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + CA9CB7492A411ECD003BDB3B /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"Standups/Preview Content\""; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = Standups/Info.plist; + INFOPLIST_KEY_NSSpeechRecognitionUsageDescription = "To record meetings"; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = co.pointfree.Standups; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + CA9CB74A2A411ECD003BDB3B /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"Standups/Preview Content\""; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = Standups/Info.plist; + INFOPLIST_KEY_NSSpeechRecognitionUsageDescription = "To record meetings"; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = co.pointfree.Standups; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + CA9CB74C2A411ECD003BDB3B /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = co.pointfree.StandupsTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Standups.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Standups"; + }; + name = Debug; + }; + CA9CB74D2A411ECD003BDB3B /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = co.pointfree.StandupsTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Standups.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Standups"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + CA9CB71F2A411ECC003BDB3B /* Build configuration list for PBXProject "Standups" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + CA9CB7462A411ECD003BDB3B /* Debug */, + CA9CB7472A411ECD003BDB3B /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + CA9CB7482A411ECD003BDB3B /* Build configuration list for PBXNativeTarget "Standups" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + CA9CB7492A411ECD003BDB3B /* Debug */, + CA9CB74A2A411ECD003BDB3B /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + CA9CB74B2A411ECD003BDB3B /* Build configuration list for PBXNativeTarget "StandupsTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + CA9CB74C2A411ECD003BDB3B /* Debug */, + CA9CB74D2A411ECD003BDB3B /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + CA6D66502A68263900B2A77A /* XCRemoteSwiftPackageReference "swift-composable-architecture" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/pointfreeco/swift-composable-architecture.git"; + requirement = { + branch = 1.0.0; + kind = branch; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + CA6D66512A68263900B2A77A /* ComposableArchitecture */ = { + isa = XCSwiftPackageProductDependency; + package = CA6D66502A68263900B2A77A /* XCRemoteSwiftPackageReference "swift-composable-architecture" */; + productName = ComposableArchitecture; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = CA9CB71C2A411ECC003BDB3B /* Project object */; +} diff --git a/0247-tca-tour-pt5/Standups/Standups.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/0247-tca-tour-pt5/Standups/Standups.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..919434a6 --- /dev/null +++ b/0247-tca-tour-pt5/Standups/Standups.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/0247-tca-tour-pt5/Standups/Standups.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/0247-tca-tour-pt5/Standups/Standups.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/0247-tca-tour-pt5/Standups/Standups.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/0247-tca-tour-pt5/Standups/Standups.xcodeproj/xcshareddata/xcschemes/Standups.xcscheme b/0247-tca-tour-pt5/Standups/Standups.xcodeproj/xcshareddata/xcschemes/Standups.xcscheme new file mode 100644 index 00000000..63832754 --- /dev/null +++ b/0247-tca-tour-pt5/Standups/Standups.xcodeproj/xcshareddata/xcschemes/Standups.xcscheme @@ -0,0 +1,106 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/0247-tca-tour-pt5/Standups/Standups/App.swift b/0247-tca-tour-pt5/Standups/Standups/App.swift new file mode 100644 index 00000000..0675caf7 --- /dev/null +++ b/0247-tca-tour-pt5/Standups/Standups/App.swift @@ -0,0 +1,107 @@ +import ComposableArchitecture +import SwiftUI + +struct AppFeature: Reducer { + struct State: Equatable { + var path = StackState() + var standupsList = StandupsListFeature.State() + } + enum Action: Equatable { + case path(StackAction) + case standupsList(StandupsListFeature.Action) + } + + struct Path: Reducer { + enum State: Equatable { + case detail(StandupDetailFeature.State) + case recordMeeting(RecordMeetingFeature.State) + } + enum Action: Equatable { + case detail(StandupDetailFeature.Action) + case recordMeeting(RecordMeetingFeature.Action) + } + var body: some ReducerOf { + Scope(state: /State.detail, action: /Action.detail) { + StandupDetailFeature() + } + Scope(state: /State.recordMeeting, action: /Action.recordMeeting) { + RecordMeetingFeature() + } + } + } + + var body: some ReducerOf { + Scope(state: \.standupsList, action: /Action.standupsList) { + StandupsListFeature() + } + + Reduce { state, action in + switch action { + case let .path(.element(id: _, action: .detail(.delegate(action)))): + switch action { + case let .deleteStandup(id: id): + state.standupsList.standups.remove(id: id) + return .none + + case let .standupUpdated(standup): + state.standupsList.standups[id: standup.id] = standup + return .none + } + + case .path: + return .none + + case .standupsList: + return .none + } + } + .forEach(\.path, action: /Action.path) { + Path() + } + } +} + +struct AppView: View { + let store: StoreOf + + var body: some View { + NavigationStackStore( + self.store.scope(state: \.path, action: { .path($0) }) + ) { + StandupsListView( + store: self.store.scope( + state: \.standupsList, + action: { .standupsList($0) } + ) + ) + } destination: { state in + switch state { + case .detail: + CaseLet( + /AppFeature.Path.State.detail, + action: AppFeature.Path.Action.detail, + then: StandupDetailView.init(store:) + ) + case .recordMeeting: + CaseLet( + /AppFeature.Path.State.recordMeeting, + action: AppFeature.Path.Action.recordMeeting, + then: RecordMeetingView.init(store:) + ) + } + } + } +} + +#Preview { + AppView( + store: Store( + initialState: AppFeature.State( + standupsList: StandupsListFeature.State(standups: [.mock]) + ) + ) { + AppFeature() + ._printChanges() + } + ) +} diff --git a/0247-tca-tour-pt5/Standups/Standups/Assets.xcassets/AccentColor.colorset/Contents.json b/0247-tca-tour-pt5/Standups/Standups/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 00000000..eb878970 --- /dev/null +++ b/0247-tca-tour-pt5/Standups/Standups/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/0247-tca-tour-pt5/Standups/Standups/Assets.xcassets/AppIcon.appiconset/Contents.json b/0247-tca-tour-pt5/Standups/Standups/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..13613e3e --- /dev/null +++ b/0247-tca-tour-pt5/Standups/Standups/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,13 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/0247-tca-tour-pt5/Standups/Standups/Assets.xcassets/Contents.json b/0247-tca-tour-pt5/Standups/Standups/Assets.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/0247-tca-tour-pt5/Standups/Standups/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/0247-tca-tour-pt5/Standups/Standups/Assets.xcassets/Themes/Contents.json b/0247-tca-tour-pt5/Standups/Standups/Assets.xcassets/Themes/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/0247-tca-tour-pt5/Standups/Standups/Assets.xcassets/Themes/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/0247-tca-tour-pt5/Standups/Standups/Assets.xcassets/Themes/bubblegum.colorset/Contents.json b/0247-tca-tour-pt5/Standups/Standups/Assets.xcassets/Themes/bubblegum.colorset/Contents.json new file mode 100644 index 00000000..849c4cbf --- /dev/null +++ b/0247-tca-tour-pt5/Standups/Standups/Assets.xcassets/Themes/bubblegum.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.820", + "green" : "0.502", + "red" : "0.933" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.820", + "green" : "0.502", + "red" : "0.933" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/0247-tca-tour-pt5/Standups/Standups/Assets.xcassets/Themes/buttercup.colorset/Contents.json b/0247-tca-tour-pt5/Standups/Standups/Assets.xcassets/Themes/buttercup.colorset/Contents.json new file mode 100644 index 00000000..92c0b5a8 --- /dev/null +++ b/0247-tca-tour-pt5/Standups/Standups/Assets.xcassets/Themes/buttercup.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.588", + "green" : "0.945", + "red" : "1.000" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.588", + "green" : "0.945", + "red" : "1.000" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/0247-tca-tour-pt5/Standups/Standups/Assets.xcassets/Themes/indigo.colorset/Contents.json b/0247-tca-tour-pt5/Standups/Standups/Assets.xcassets/Themes/indigo.colorset/Contents.json new file mode 100644 index 00000000..d9daea3e --- /dev/null +++ b/0247-tca-tour-pt5/Standups/Standups/Assets.xcassets/Themes/indigo.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.443", + "green" : "0.000", + "red" : "0.212" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.443", + "green" : "0.000", + "red" : "0.212" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/0247-tca-tour-pt5/Standups/Standups/Assets.xcassets/Themes/lavender.colorset/Contents.json b/0247-tca-tour-pt5/Standups/Standups/Assets.xcassets/Themes/lavender.colorset/Contents.json new file mode 100644 index 00000000..f95edce0 --- /dev/null +++ b/0247-tca-tour-pt5/Standups/Standups/Assets.xcassets/Themes/lavender.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "0.808", + "red" : "0.812" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "0.808", + "red" : "0.812" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/0247-tca-tour-pt5/Standups/Standups/Assets.xcassets/Themes/magenta.colorset/Contents.json b/0247-tca-tour-pt5/Standups/Standups/Assets.xcassets/Themes/magenta.colorset/Contents.json new file mode 100644 index 00000000..b20bdf59 --- /dev/null +++ b/0247-tca-tour-pt5/Standups/Standups/Assets.xcassets/Themes/magenta.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.467", + "green" : "0.075", + "red" : "0.647" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.467", + "green" : "0.075", + "red" : "0.647" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/0247-tca-tour-pt5/Standups/Standups/Assets.xcassets/Themes/navy.colorset/Contents.json b/0247-tca-tour-pt5/Standups/Standups/Assets.xcassets/Themes/navy.colorset/Contents.json new file mode 100644 index 00000000..821f22f7 --- /dev/null +++ b/0247-tca-tour-pt5/Standups/Standups/Assets.xcassets/Themes/navy.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.255", + "green" : "0.078", + "red" : "0.000" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.255", + "green" : "0.078", + "red" : "0.000" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/0247-tca-tour-pt5/Standups/Standups/Assets.xcassets/Themes/orange.colorset/Contents.json b/0247-tca-tour-pt5/Standups/Standups/Assets.xcassets/Themes/orange.colorset/Contents.json new file mode 100644 index 00000000..863c8c72 --- /dev/null +++ b/0247-tca-tour-pt5/Standups/Standups/Assets.xcassets/Themes/orange.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.259", + "green" : "0.545", + "red" : "1.000" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.259", + "green" : "0.545", + "red" : "1.000" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/0247-tca-tour-pt5/Standups/Standups/Assets.xcassets/Themes/oxblood.colorset/Contents.json b/0247-tca-tour-pt5/Standups/Standups/Assets.xcassets/Themes/oxblood.colorset/Contents.json new file mode 100644 index 00000000..0821af29 --- /dev/null +++ b/0247-tca-tour-pt5/Standups/Standups/Assets.xcassets/Themes/oxblood.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.043", + "green" : "0.027", + "red" : "0.290" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.043", + "green" : "0.027", + "red" : "0.290" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/0247-tca-tour-pt5/Standups/Standups/Assets.xcassets/Themes/periwinkle.colorset/Contents.json b/0247-tca-tour-pt5/Standups/Standups/Assets.xcassets/Themes/periwinkle.colorset/Contents.json new file mode 100644 index 00000000..8d29c91c --- /dev/null +++ b/0247-tca-tour-pt5/Standups/Standups/Assets.xcassets/Themes/periwinkle.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "0.510", + "red" : "0.525" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "0.510", + "red" : "0.525" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/0247-tca-tour-pt5/Standups/Standups/Assets.xcassets/Themes/poppy.colorset/Contents.json b/0247-tca-tour-pt5/Standups/Standups/Assets.xcassets/Themes/poppy.colorset/Contents.json new file mode 100644 index 00000000..d6a984fc --- /dev/null +++ b/0247-tca-tour-pt5/Standups/Standups/Assets.xcassets/Themes/poppy.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.369", + "green" : "0.369", + "red" : "1.000" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.369", + "green" : "0.369", + "red" : "1.000" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/0247-tca-tour-pt5/Standups/Standups/Assets.xcassets/Themes/purple.colorset/Contents.json b/0247-tca-tour-pt5/Standups/Standups/Assets.xcassets/Themes/purple.colorset/Contents.json new file mode 100644 index 00000000..b19089a1 --- /dev/null +++ b/0247-tca-tour-pt5/Standups/Standups/Assets.xcassets/Themes/purple.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.949", + "green" : "0.294", + "red" : "0.569" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.949", + "green" : "0.294", + "red" : "0.569" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/0247-tca-tour-pt5/Standups/Standups/Assets.xcassets/Themes/seafoam.colorset/Contents.json b/0247-tca-tour-pt5/Standups/Standups/Assets.xcassets/Themes/seafoam.colorset/Contents.json new file mode 100644 index 00000000..39065d2a --- /dev/null +++ b/0247-tca-tour-pt5/Standups/Standups/Assets.xcassets/Themes/seafoam.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.898", + "green" : "0.918", + "red" : "0.796" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.898", + "green" : "0.918", + "red" : "0.796" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/0247-tca-tour-pt5/Standups/Standups/Assets.xcassets/Themes/sky.colorset/Contents.json b/0247-tca-tour-pt5/Standups/Standups/Assets.xcassets/Themes/sky.colorset/Contents.json new file mode 100644 index 00000000..91e82482 --- /dev/null +++ b/0247-tca-tour-pt5/Standups/Standups/Assets.xcassets/Themes/sky.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "0.573", + "red" : "0.431" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "0.573", + "red" : "0.431" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/0247-tca-tour-pt5/Standups/Standups/Assets.xcassets/Themes/tan.colorset/Contents.json b/0247-tca-tour-pt5/Standups/Standups/Assets.xcassets/Themes/tan.colorset/Contents.json new file mode 100644 index 00000000..e42a6726 --- /dev/null +++ b/0247-tca-tour-pt5/Standups/Standups/Assets.xcassets/Themes/tan.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.494", + "green" : "0.608", + "red" : "0.761" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.494", + "green" : "0.608", + "red" : "0.761" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/0247-tca-tour-pt5/Standups/Standups/Assets.xcassets/Themes/teal.colorset/Contents.json b/0247-tca-tour-pt5/Standups/Standups/Assets.xcassets/Themes/teal.colorset/Contents.json new file mode 100644 index 00000000..a43d6577 --- /dev/null +++ b/0247-tca-tour-pt5/Standups/Standups/Assets.xcassets/Themes/teal.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.620", + "green" : "0.561", + "red" : "0.133" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.620", + "green" : "0.561", + "red" : "0.133" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/0247-tca-tour-pt5/Standups/Standups/Assets.xcassets/Themes/yellow.colorset/Contents.json b/0247-tca-tour-pt5/Standups/Standups/Assets.xcassets/Themes/yellow.colorset/Contents.json new file mode 100644 index 00000000..ce3b3be8 --- /dev/null +++ b/0247-tca-tour-pt5/Standups/Standups/Assets.xcassets/Themes/yellow.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.302", + "green" : "0.875", + "red" : "1.000" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.302", + "green" : "0.875", + "red" : "1.000" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/0247-tca-tour-pt5/Standups/Standups/Info.plist b/0247-tca-tour-pt5/Standups/Standups/Info.plist new file mode 100644 index 00000000..0c67376e --- /dev/null +++ b/0247-tca-tour-pt5/Standups/Standups/Info.plist @@ -0,0 +1,5 @@ + + + + + diff --git a/0247-tca-tour-pt5/Standups/Standups/Models.swift b/0247-tca-tour-pt5/Standups/Standups/Models.swift new file mode 100644 index 00000000..5ebc6a59 --- /dev/null +++ b/0247-tca-tour-pt5/Standups/Standups/Models.swift @@ -0,0 +1,91 @@ +import SwiftUI + +struct Standup: Equatable, Identifiable, Codable { + let id: UUID + var attendees: [Attendee] = [] + var duration = Duration.seconds(60 * 5) + var meetings: [Meeting] = [] + var theme: Theme = .bubblegum + var title = "" + + var durationPerAttendee: Duration { + self.duration / self.attendees.count + } +} + +struct Attendee: Equatable, Identifiable, Codable { + let id: UUID + var name = "" +} + +struct Meeting: Equatable, Identifiable, Codable { + let id: UUID + let date: Date + var transcript: String +} + +enum Theme: String, CaseIterable, Equatable, Hashable, Identifiable, Codable { + case bubblegum + case buttercup + case indigo + case lavender + case magenta + case navy + case orange + case oxblood + case periwinkle + case poppy + case purple + case seafoam + case sky + case tan + case teal + case yellow + + var id: Self { self } + + var accentColor: Color { + switch self { + case .bubblegum, .buttercup, .lavender, .orange, .periwinkle, .poppy, .seafoam, .sky, .tan, + .teal, .yellow: + return .black + case .indigo, .magenta, .navy, .oxblood, .purple: + return .white + } + } + + var mainColor: Color { Color(self.rawValue) } + + var name: String { self.rawValue.capitalized } +} + +extension Standup { + static let mock = Self( + id: Standup.ID(), + attendees: [ + Attendee(id: Attendee.ID(), name: "Blob"), + Attendee(id: Attendee.ID(), name: "Blob Jr"), + Attendee(id: Attendee.ID(), name: "Blob Sr"), + Attendee(id: Attendee.ID(), name: "Blob Esq"), + Attendee(id: Attendee.ID(), name: "Blob III"), + Attendee(id: Attendee.ID(), name: "Blob I"), + ], + duration: .seconds(60), + meetings: [ + Meeting( + id: Meeting.ID(), + date: Date().addingTimeInterval(-60 * 60 * 24 * 7), + transcript: """ + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor \ + incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud \ + exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure \ + dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. \ + Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt \ + mollit anim id est laborum. + """ + ) + ], + theme: .orange, + title: "Design" + ) +} diff --git a/0247-tca-tour-pt5/Standups/Standups/Preview Content/Preview Assets.xcassets/Contents.json b/0247-tca-tour-pt5/Standups/Standups/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/0247-tca-tour-pt5/Standups/Standups/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/0247-tca-tour-pt5/Standups/Standups/RecordMeeting.swift b/0247-tca-tour-pt5/Standups/Standups/RecordMeeting.swift new file mode 100644 index 00000000..473cc451 --- /dev/null +++ b/0247-tca-tour-pt5/Standups/Standups/RecordMeeting.swift @@ -0,0 +1,254 @@ +import ComposableArchitecture +import Speech +import SwiftUI + +struct RecordMeetingFeature: Reducer { + struct State: Equatable { + var secondsElapsed = 0 + var speakerIndex = 0 + let standup: Standup + + var durationRemaining: Duration { + self.standup.duration - .seconds(self.secondsElapsed) + } + } + enum Action: Equatable { + case endMeetingButtonTapped + case nextButtonTapped + case onTask + case timerTicked + } + @Dependency(\.continuousClock) var clock + var body: some ReducerOf { + Reduce { state, action in + switch action { + case .endMeetingButtonTapped: + return .none + + case .nextButtonTapped: + return .none + + case .onTask: + return .run { send in + let status = await withUnsafeContinuation { continuation in + SFSpeechRecognizer.requestAuthorization { status in + continuation.resume(with: .success(status)) + } + } + for await _ in self.clock.timer(interval: .seconds(1)) { + await send(.timerTicked) + } + } + + case .timerTicked: + state.secondsElapsed += 1 + return .none + } + } + } +} + +struct RecordMeetingView: View { + let store: StoreOf + + var body: some View { + WithViewStore(self.store, observe: { $0 }) { viewStore in + ZStack { + RoundedRectangle(cornerRadius: 16) + .fill(viewStore.standup.theme.mainColor) + + VStack { + MeetingHeaderView( + secondsElapsed: viewStore.secondsElapsed, + durationRemaining: viewStore.durationRemaining, + theme: viewStore.standup.theme + ) + MeetingTimerView( + standup: viewStore.standup, + speakerIndex: viewStore.speakerIndex + ) + MeetingFooterView( + standup: viewStore.standup, + nextButtonTapped: { + viewStore.send(.nextButtonTapped) + }, + speakerIndex: viewStore.speakerIndex + ) + } + } + .padding() + .foregroundColor(viewStore.standup.theme.accentColor) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button("End meeting") { + viewStore.send(.endMeetingButtonTapped) + } + } + } + .navigationBarBackButtonHidden(true) + .task { await viewStore.send(.onTask).finish() } + } + } +} + +struct MeetingHeaderView: View { + let secondsElapsed: Int + let durationRemaining: Duration + let theme: Theme + + var body: some View { + VStack { + ProgressView(value: self.progress) + .progressViewStyle(MeetingProgressViewStyle(theme: self.theme)) + HStack { + VStack(alignment: .leading) { + Text("Time Elapsed") + .font(.caption) + Label( + Duration.seconds(self.secondsElapsed).formatted(.units()), + systemImage: "hourglass.bottomhalf.fill" + ) + } + Spacer() + VStack(alignment: .trailing) { + Text("Time Remaining") + .font(.caption) + Label(self.durationRemaining.formatted(.units()), systemImage: "hourglass.tophalf.fill") + .font(.body.monospacedDigit()) + .labelStyle(.trailingIcon) + } + } + } + .padding([.top, .horizontal]) + } + + private var totalDuration: Duration { + .seconds(self.secondsElapsed) + self.durationRemaining + } + + private var progress: Double { + guard self.totalDuration > .seconds(0) else { return 0 } + return Double(self.secondsElapsed) / Double(self.totalDuration.components.seconds) + } +} + +struct MeetingProgressViewStyle: ProgressViewStyle { + var theme: Theme + + func makeBody(configuration: Configuration) -> some View { + ZStack { + RoundedRectangle(cornerRadius: 10) + .fill(self.theme.accentColor) + .frame(height: 20) + + ProgressView(configuration) + .tint(self.theme.mainColor) + .frame(height: 12) + .padding(.horizontal) + } + } +} + +struct MeetingTimerView: View { + let standup: Standup + let speakerIndex: Int + + var body: some View { + Circle() + .strokeBorder(lineWidth: 24) + .overlay { + VStack { + Group { + if self.speakerIndex < self.standup.attendees.count { + Text(self.standup.attendees[self.speakerIndex].name) + } else { + Text("Someone") + } + } + .font(.title) + Text("is speaking") + Image(systemName: "mic.fill") + .font(.largeTitle) + .padding(.top) + } + .foregroundStyle(self.standup.theme.accentColor) + } + .overlay { + ForEach(Array(self.standup.attendees.enumerated()), id: \.element.id) { index, attendee in + if index < self.speakerIndex + 1 { + SpeakerArc(totalSpeakers: self.standup.attendees.count, speakerIndex: index) + .rotation(Angle(degrees: -90)) + .stroke(self.standup.theme.mainColor, lineWidth: 12) + } + } + } + .padding(.horizontal) + } +} + +struct SpeakerArc: Shape { + let totalSpeakers: Int + let speakerIndex: Int + + func path(in rect: CGRect) -> Path { + let diameter = min(rect.size.width, rect.size.height) - 24 + let radius = diameter / 2 + let center = CGPoint(x: rect.midX, y: rect.midY) + return Path { path in + path.addArc( + center: center, + radius: radius, + startAngle: self.startAngle, + endAngle: self.endAngle, + clockwise: false + ) + } + } + + private var degreesPerSpeaker: Double { + 360 / Double(self.totalSpeakers) + } + private var startAngle: Angle { + Angle(degrees: self.degreesPerSpeaker * Double(self.speakerIndex) + 1) + } + private var endAngle: Angle { + Angle(degrees: self.startAngle.degrees + self.degreesPerSpeaker - 1) + } +} + +struct MeetingFooterView: View { + let standup: Standup + var nextButtonTapped: () -> Void + let speakerIndex: Int + + var body: some View { + VStack { + HStack { + if self.speakerIndex < self.standup.attendees.count - 1 { + Text("Speaker \(self.speakerIndex + 1) of \(self.standup.attendees.count)") + } else { + Text("No more speakers.") + } + Spacer() + Button(action: self.nextButtonTapped) { + Image(systemName: "forward.fill") + } + } + } + .padding([.bottom, .horizontal]) + } +} + +#Preview { + MainActor.assumeIsolated { + NavigationStack { + RecordMeetingView( + store: Store(initialState: RecordMeetingFeature.State(standup: .mock)) { + RecordMeetingFeature() + } + ) + } + } +} + diff --git a/0247-tca-tour-pt5/Standups/Standups/StandupDetail.swift b/0247-tca-tour-pt5/Standups/Standups/StandupDetail.swift new file mode 100644 index 00000000..df2faf5b --- /dev/null +++ b/0247-tca-tour-pt5/Standups/Standups/StandupDetail.swift @@ -0,0 +1,217 @@ +import ComposableArchitecture +import SwiftUI + +struct StandupDetailFeature: Reducer { + struct State: Equatable { + @PresentationState var destination: Destination.State? + var standup: Standup + } + enum Action: Equatable { + case cancelEditStandupButtonTapped + case delegate(Delegate) + case deleteButtonTapped + case deleteMeetings(atOffsets: IndexSet) + case destination(PresentationAction) + case editButtonTapped + case saveStandupButtonTapped + enum Delegate: Equatable { + case deleteStandup(id: Standup.ID) + case standupUpdated(Standup) + } + } + @Dependency(\.dismiss) var dismiss + + // @Environment(\.dismiss) var dismiss + + struct Destination: Reducer { + enum State: Equatable { + case alert(AlertState) + case editStandup(StandupFormFeature.State) + } + enum Action: Equatable { + case alert(Alert) + case editStandup(StandupFormFeature.Action) + enum Alert { + case confirmDeletion + } + } + var body: some ReducerOf { + Scope(state: /State.editStandup, action: /Action.editStandup) { + StandupFormFeature() + } + } + } + + var body: some ReducerOf { + Reduce { state, action in + switch action { + case .cancelEditStandupButtonTapped: + state.destination = nil + return .none + + case .delegate: + return .none + + case .deleteButtonTapped: + state.destination = .alert( + AlertState { + TextState("Are you sure you want to delete?") + } actions: { + ButtonState(role: .destructive, action: .confirmDeletion) { + TextState("Delete") + } + } + ) + return .none + + case .deleteMeetings(atOffsets: let indices): + state.standup.meetings.remove(atOffsets: indices) + return .none + + case .destination(.presented(.alert(.confirmDeletion))): + // TODO: Delete this standup + return .run { [id = state.standup.id] send in + await send(.delegate(.deleteStandup(id: id))) + await self.dismiss() + } + + case .destination: + return .none + + case .editButtonTapped: + state.destination = .editStandup(StandupFormFeature.State(standup: state.standup)) + return .none + case .saveStandupButtonTapped: + guard case let .editStandup(standupForm) = state.destination + else { return .none } + state.standup = standupForm.standup + state.destination = nil + return .none + } + } + .ifLet(\.$destination, action: /Action.destination) { + Destination() + } + .onChange(of: \.standup) { oldValue, newValue in + Reduce { state, action in + .send(.delegate(.standupUpdated(newValue))) + } + } + } +} + +struct StandupDetailView: View { + let store: StoreOf + + var body: some View { + WithViewStore(self.store, observe: { $0 }) { viewStore in + List { + Section { + NavigationLink( + state: AppFeature.Path.State.recordMeeting(RecordMeetingFeature.State(standup: viewStore.standup)) + ) { + Label("Start Meeting", systemImage: "timer") + .font(.headline) + .foregroundColor(.accentColor) + } + HStack { + Label("Length", systemImage: "clock") + Spacer() + Text(viewStore.standup.duration.formatted(.units())) + } + + HStack { + Label("Theme", systemImage: "paintpalette") + Spacer() + Text(viewStore.standup.theme.name) + .padding(4) + .foregroundColor(viewStore.standup.theme.accentColor) + .background(viewStore.standup.theme.mainColor) + .cornerRadius(4) + } + } header: { + Text("Standup Info") + } + + if !viewStore.standup.meetings.isEmpty { + Section { + ForEach(viewStore.standup.meetings) { meeting in + NavigationLink { + /*@START_MENU_TOKEN@*//*@PLACEHOLDER=Do something@*//*@END_MENU_TOKEN@*/ + } label: { + HStack { + Image(systemName: "calendar") + Text(meeting.date, style: .date) + Text(meeting.date, style: .time) + } + } + } + .onDelete { indices in + viewStore.send(.deleteMeetings(atOffsets: indices)) + } + } header: { + Text("Past meetings") + } + } + + Section { + ForEach(viewStore.standup.attendees) { attendee in + Label(attendee.name, systemImage: "person") + } + } header: { + Text("Attendees") + } + + Section { + Button("Delete") { + viewStore.send(.deleteButtonTapped) + } + .foregroundColor(.red) + .frame(maxWidth: .infinity) + } + } + .navigationTitle(viewStore.standup.title) + .toolbar { + Button("Edit") { + viewStore.send(.editButtonTapped) + } + } + .alert( + store: self.store.scope(state: \.$destination, action: { .destination($0) }), + state: /StandupDetailFeature.Destination.State.alert, + action: StandupDetailFeature.Destination.Action.alert + ) + .sheet( + store: self.store.scope(state: \.$destination, action: { .destination($0) }), + state: /StandupDetailFeature.Destination.State.editStandup, + action: StandupDetailFeature.Destination.Action.editStandup + ) { store in + NavigationStack { + StandupFormView(store: store) + .navigationTitle("Edit standup") + .toolbar { + ToolbarItem { + Button("Save") { viewStore.send(.saveStandupButtonTapped) } + } + ToolbarItem(placement: .cancellationAction) { + Button("Cancel") { viewStore.send(.cancelEditStandupButtonTapped) } + } + } + } + } + } + } +} + +#Preview { + MainActor.assumeIsolated { + NavigationStack { + StandupDetailView( + store: Store(initialState: StandupDetailFeature.State(standup: .mock)) { + StandupDetailFeature() + ._printChanges() + } + ) + } + } +} diff --git a/0247-tca-tour-pt5/Standups/Standups/StandupForm.swift b/0247-tca-tour-pt5/Standups/Standups/StandupForm.swift new file mode 100644 index 00000000..4a71505d --- /dev/null +++ b/0247-tca-tour-pt5/Standups/Standups/StandupForm.swift @@ -0,0 +1,136 @@ +import ComposableArchitecture +import SwiftUI + +struct StandupFormFeature: Reducer { + struct State: Equatable { + @BindingState var focus: Field? + @BindingState var standup: Standup + + enum Field: Hashable { + case attendee(Attendee.ID) + case title + } + + init(focus: Field? = .title, standup: Standup) { + self.focus = focus + self.standup = standup + if self.standup.attendees.isEmpty { + @Dependency(\.uuid) var uuid + self.standup.attendees.append(Attendee(id: uuid())) + } + } + } + enum Action: BindableAction, Equatable { + case addAttendeeButtonTapped + case binding(BindingAction) + case deleteAttendees(atOffsets: IndexSet) + } + @Dependency(\.uuid) var uuid + var body: some ReducerOf { + BindingReducer() + Reduce { state, action in + switch action { + case .addAttendeeButtonTapped: + let id = self.uuid() + state.standup.attendees.append(Attendee(id: id)) + state.focus = .attendee(id) + return .none + + case .binding(_): + return .none + + case let .deleteAttendees(atOffsets: indices): + state.standup.attendees.remove(atOffsets: indices) + if state.standup.attendees.isEmpty { + state.standup.attendees.append(Attendee(id: self.uuid())) + } + guard let firstIndex = indices.first + else { return .none } + let index = min(firstIndex, state.standup.attendees.count - 1) + state.focus = .attendee(state.standup.attendees[index].id) + return .none + } + } + } +} + +struct StandupFormView: View { + let store: StoreOf + @FocusState var focus: StandupFormFeature.State.Field? + + var body: some View { + WithViewStore(self.store, observe: { $0 }) { viewStore in + Form { + Section { + TextField("Title", text: viewStore.$standup.title) + .focused(self.$focus, equals: .title) + HStack { + Slider(value: viewStore.$standup.duration.minutes, in: 5...30, step: 1) { + Text("Length") + } + Spacer() + Text(viewStore.standup.duration.formatted(.units())) + } + ThemePicker(selection: viewStore.$standup.theme) + } header: { + Text("Standup Info") + } + Section { + ForEach(viewStore.$standup.attendees) { $attendee in + TextField("Name", text: $attendee.name) + .focused(self.$focus, equals: .attendee(attendee.id)) + } + .onDelete { indices in + viewStore.send(.deleteAttendees(atOffsets: indices)) + } + + Button("Add attendee") { + viewStore.send(.addAttendeeButtonTapped) + } + } header: { + Text("Attendees") + } + } + .bind(viewStore.$focus, to: self.$focus) + } + } +} + +extension Duration { + fileprivate var minutes: Double { + get { Double(self.components.seconds / 60) } + set { self = .seconds(newValue * 60) } + } +} + +struct ThemePicker: View { + @Binding var selection: Theme + + var body: some View { + Picker("Theme", selection: self.$selection) { + ForEach(Theme.allCases) { theme in + ZStack { + RoundedRectangle(cornerRadius: 4) + .fill(theme.mainColor) + Label(theme.name, systemImage: "paintpalette") + .padding(4) + } + .foregroundColor(theme.accentColor) + .fixedSize(horizontal: false, vertical: true) + .tag(theme) + } + } + } +} + +#Preview { + MainActor.assumeIsolated { + NavigationStack { + StandupFormView( + store: Store(initialState: StandupFormFeature.State(standup: .mock)) { + StandupFormFeature() + } + ) + } + } +} diff --git a/0247-tca-tour-pt5/Standups/Standups/StandupsApp.swift b/0247-tca-tour-pt5/Standups/Standups/StandupsApp.swift new file mode 100644 index 00000000..90adb537 --- /dev/null +++ b/0247-tca-tour-pt5/Standups/Standups/StandupsApp.swift @@ -0,0 +1,24 @@ +import ComposableArchitecture +import SwiftUI + +@main +struct StandupsApp: App { + var body: some Scene { + WindowGroup { + AppView( + store: Store( + initialState: AppFeature.State( + path: StackState([ +// .detail(StandupDetailFeature.State(standup: .mock)), +// .recordMeeting(RecordMeetingFeature.State()), + ]), + standupsList: StandupsListFeature.State(standups: [.mock]) + ) + ) { + AppFeature() + ._printChanges() + } + ) + } + } +} diff --git a/0247-tca-tour-pt5/Standups/Standups/StandupsList.swift b/0247-tca-tour-pt5/Standups/Standups/StandupsList.swift new file mode 100644 index 00000000..ccff4968 --- /dev/null +++ b/0247-tca-tour-pt5/Standups/Standups/StandupsList.swift @@ -0,0 +1,139 @@ +import ComposableArchitecture +import SwiftUI + +struct StandupsListFeature: Reducer { + struct State: Equatable { + @PresentationState var addStandup: StandupFormFeature.State? + var standups: IdentifiedArrayOf = [] + } + enum Action: Equatable { + case addButtonTapped + case addStandup(PresentationAction) + case cancelStandupButtonTapped + case saveStandupButtonTapped + } + @Dependency(\.uuid) var uuid + var body: some ReducerOf { + Reduce { state, action in + switch action { + case .addButtonTapped: + state.addStandup = StandupFormFeature.State(standup: Standup(id: self.uuid())) + return .none + + case .addStandup: + return .none + + case .cancelStandupButtonTapped: + state.addStandup = nil + return .none + + case .saveStandupButtonTapped: + guard let standup = state.addStandup?.standup + else { return .none } + state.standups.append(standup) + state.addStandup = nil + return .none + } + } + .ifLet(\.$addStandup, action: /Action.addStandup) { + StandupFormFeature() + } + } +} + +struct StandupsListView: View { + let store: StoreOf + + var body: some View { + WithViewStore(self.store, observe: \.standups) { viewStore in + List { + ForEach(viewStore.state) { standup in + NavigationLink( + state: AppFeature.Path.State.detail(StandupDetailFeature.State(standup: standup)) + ) { + CardView(standup: standup) + } + .listRowBackground(standup.theme.mainColor) + } + } + .navigationTitle("Daily Standups") + .toolbar { + ToolbarItem { + Button("Add") { + viewStore.send(.addButtonTapped) + } + } + } + .sheet( + store: self.store.scope( + state: \.$addStandup, + action: { .addStandup($0) } + ) + ) { store in + NavigationStack { + StandupFormView(store: store) + .navigationTitle("New standup") + .toolbar { + ToolbarItem { + Button("Save") { viewStore.send(.saveStandupButtonTapped) } + } + ToolbarItem(placement: .cancellationAction) { + Button("Cancel") { viewStore.send(.cancelStandupButtonTapped) } + } + } + } + } + } + } +} + +struct CardView: View { + let standup: Standup + + var body: some View { + VStack(alignment: .leading) { + Text(self.standup.title) + .font(.headline) + Spacer() + HStack { + Label("\(self.standup.attendees.count)", systemImage: "person.3") + Spacer() + Label(self.standup.duration.formatted(.units()), systemImage: "clock") + .labelStyle(.trailingIcon) + } + .font(.caption) + } + .padding() + .foregroundColor(self.standup.theme.accentColor) + } +} + +struct TrailingIconLabelStyle: LabelStyle { + func makeBody(configuration: Configuration) -> some View { + HStack { + configuration.title + configuration.icon + } + } +} + +extension LabelStyle where Self == TrailingIconLabelStyle { + static var trailingIcon: Self { Self() } +} + +#Preview { + MainActor.assumeIsolated { + NavigationStack { + StandupsListView( + store: Store( + initialState: StandupsListFeature.State( + standups: [.mock] + ) + ) { + StandupsListFeature() + ._printChanges() + } + ) + } + } +} diff --git a/0247-tca-tour-pt5/Standups/StandupsTests/AppTests.swift b/0247-tca-tour-pt5/Standups/StandupsTests/AppTests.swift new file mode 100644 index 00000000..8e722b0c --- /dev/null +++ b/0247-tca-tour-pt5/Standups/StandupsTests/AppTests.swift @@ -0,0 +1,89 @@ +import ComposableArchitecture +import XCTest +@testable import Standups + +@MainActor +final class AppTests: XCTestCase { + func testEdit() async { + let standup = Standup.mock + let store = TestStore( + initialState: AppFeature.State( + standupsList: StandupsListFeature.State( + standups: [standup] + ) + ) + ) { + AppFeature() + } + await store.send(.path(.push(id: 0, state: .detail(StandupDetailFeature.State(standup: standup))))) { + $0.path[id: 0] = .detail(StandupDetailFeature.State(standup: standup)) + } + await store.send(.path(.element(id: 0, action: .detail(.editButtonTapped)))) { + $0.path[id: 0, case: /AppFeature.Path.State.detail]?.destination = .editStandup(StandupFormFeature.State(standup: standup)) + } + var editedStandup = standup + editedStandup.title = "Point-Free Morning Sync" + await store.send(.path(.element(id: 0, action: .detail(.destination(.presented(.editStandup(.set(\.$standup, editedStandup)))))))) { + $0.path[id: 0, case: /AppFeature.Path.State.detail]? + .$destination[case: /StandupDetailFeature.Destination.State.editStandup]? + .standup.title = "Point-Free Morning Sync" + } + await store.send(.path(.element(id: 0, action: .detail(.saveStandupButtonTapped)))) { + $0.path[id: 0, case: /AppFeature.Path.State.detail]?.destination = nil + $0.path[id: 0, case: /AppFeature.Path.State.detail]?.standup.title = "Point-Free Morning Sync" + } + await store.receive(.path(.element(id: 0, action: .detail(.delegate(.standupUpdated(editedStandup)))))) { + $0.standupsList.standups[0].title = "Point-Free Morning Sync" + } + } + + func testEdit_NonExhaustive() async { + let standup = Standup.mock + let store = TestStore( + initialState: AppFeature.State( + standupsList: StandupsListFeature.State( + standups: [standup] + ) + ) + ) { + AppFeature() + } + store.exhaustivity = .off + await store.send(.path(.push(id: 0, state: .detail(StandupDetailFeature.State(standup: standup))))) + await store.send(.path(.element(id: 0, action: .detail(.editButtonTapped)))) + var editedStandup = standup + editedStandup.title = "Point-Free Morning Sync" + await store.send(.path(.element(id: 0, action: .detail(.destination(.presented(.editStandup(.set(\.$standup, editedStandup)))))))) + await store.send(.path(.element(id: 0, action: .detail(.saveStandupButtonTapped)))) + await store.skipReceivedActions() + store.assert { + $0.standupsList.standups[0].title = "Point-Free Morning Sync" + } + } + + + func testDeletion_NonExhaustive() async { + let standup = Standup.mock + let store = TestStore( + initialState: AppFeature.State( + path: StackState([ + .detail(StandupDetailFeature.State(standup: standup)) + ]), + standupsList: StandupsListFeature.State( + standups: [standup] + ) + ) + ) { + AppFeature() + } + store.exhaustivity = .off + + await store.send(.path(.element(id: 0, action: .detail(.deleteButtonTapped)))) + await store.send(.path(.element(id: 0, action: .detail(.destination(.presented(.alert(.confirmDeletion))))))) + await store.skipReceivedActions() + store.assert { + $0.path = StackState([]) + $0.standupsList.standups = [] + } + } +} diff --git a/0247-tca-tour-pt5/Standups/StandupsTests/StandupDetailTests.swift b/0247-tca-tour-pt5/Standups/StandupsTests/StandupDetailTests.swift new file mode 100644 index 00000000..b5bb464a --- /dev/null +++ b/0247-tca-tour-pt5/Standups/StandupsTests/StandupDetailTests.swift @@ -0,0 +1,22 @@ +import ComposableArchitecture +import XCTest +@testable import Standups + +@MainActor +final class StandupDetailTests: XCTestCase { + func testEdit() async throws { + var standup = Standup.mock + let store = TestStore(initialState: StandupDetailFeature.State(standup: standup)) { + StandupDetailFeature() + } + store.exhaustivity = .off + + await store.send(.editButtonTapped) + standup.title = "Point-Free Morning Sync" + await store.send(.destination(.presented(.editStandup(.set(\.$standup, standup))))) + await store.send(.saveStandupButtonTapped) { + $0.standup.title = "Point-Free Morning Sync" + } + } +} + diff --git a/0247-tca-tour-pt5/Standups/StandupsTests/StandupFormTests.swift b/0247-tca-tour-pt5/Standups/StandupsTests/StandupFormTests.swift new file mode 100644 index 00000000..5f957ef4 --- /dev/null +++ b/0247-tca-tour-pt5/Standups/StandupsTests/StandupFormTests.swift @@ -0,0 +1,34 @@ +import ComposableArchitecture +import XCTest +@testable import Standups + +@MainActor +final class StandupFormTests: XCTestCase { + func testAddDeleteAttendee() async { + let store = TestStore( + initialState: StandupFormFeature.State( + standup: Standup( + id: UUID(), + attendees: [ + Attendee(id: UUID()) + ] + ) + ) + ) { + StandupFormFeature() + } withDependencies: { + $0.uuid = .incrementing + } + + await store.send(.addAttendeeButtonTapped) { + $0.focus = .attendee(UUID(0)) + $0.standup.attendees.append( + Attendee(id: UUID(0)) + ) + } + await store.send(.deleteAttendees(atOffsets: [1])) { + $0.focus = .attendee($0.standup.attendees[0].id) + $0.standup.attendees.remove(at: 1) + } + } +} diff --git a/0247-tca-tour-pt5/Standups/StandupsTests/Standups.xctestplan b/0247-tca-tour-pt5/Standups/StandupsTests/Standups.xctestplan new file mode 100644 index 00000000..1580b9aa --- /dev/null +++ b/0247-tca-tour-pt5/Standups/StandupsTests/Standups.xctestplan @@ -0,0 +1,29 @@ +{ + "configurations" : [ + { + "id" : "8AE6BE17-2A32-484D-BD5F-B660D0044BF9", + "name" : "Test Scheme Action", + "options" : { + + } + } + ], + "defaultOptions" : { + "codeCoverage" : false, + "targetForVariableExpansion" : { + "containerPath" : "container:Standups.xcodeproj", + "identifier" : "CA9CB7232A411ECD003BDB3B", + "name" : "Standups" + } + }, + "testTargets" : [ + { + "target" : { + "containerPath" : "container:Standups.xcodeproj", + "identifier" : "CA9CB7332A411ECD003BDB3B", + "name" : "StandupsTests" + } + } + ], + "version" : 1 +} diff --git a/0247-tca-tour-pt5/Standups/StandupsTests/StandupsListTests.swift b/0247-tca-tour-pt5/Standups/StandupsTests/StandupsListTests.swift new file mode 100644 index 00000000..f07b87c3 --- /dev/null +++ b/0247-tca-tour-pt5/Standups/StandupsTests/StandupsListTests.swift @@ -0,0 +1,60 @@ +import ComposableArchitecture +import XCTest +@testable import Standups + +@MainActor +final class StandupsListTests: XCTestCase { + func testAddStandup() async { + let store = TestStore(initialState: StandupsListFeature.State()) { + StandupsListFeature() + } withDependencies: { + $0.uuid = .incrementing + } + + var standup = Standup( + id: UUID(0), + attendees: [Attendee(id: UUID(1))] + ) + await store.send(.addButtonTapped) { + $0.addStandup = StandupFormFeature.State( + standup: standup + ) + } + standup.title = "Point-Free Morning Sync" + await store.send(.addStandup(.presented(.set(\.$standup, standup)))) { + $0.addStandup?.standup.title = "Point-Free Morning Sync" + } + await store.send(.saveStandupButtonTapped) { + $0.addStandup = nil + $0.standups[0] = Standup( + id: UUID(0), + attendees: [Attendee(id: UUID(1))], + title: "Point-Free Morning Sync" + ) + } + } + + func testAddStandup_NonExhaustive() async { + let store = TestStore(initialState: StandupsListFeature.State()) { + StandupsListFeature() + } withDependencies: { + $0.uuid = .incrementing + } + store.exhaustivity = .off(showSkippedAssertions: true) + + var standup = Standup( + id: UUID(0), + attendees: [Attendee(id: UUID(1))] + ) + await store.send(.addButtonTapped) + standup.title = "Point-Free Morning Sync" + await store.send(.addStandup(.presented(.set(\.$standup, standup)))) + await store.send(.saveStandupButtonTapped) { + $0.standups[0] = Standup( + id: UUID(0), + attendees: [Attendee(id: UUID(1))], + title: "Point-Free Morning Sync" + ) + } + } +} diff --git a/0247-tca-tour-pt5/Standups/StandupsTests/StandupsTests.swift b/0247-tca-tour-pt5/Standups/StandupsTests/StandupsTests.swift new file mode 100644 index 00000000..2e7f9792 --- /dev/null +++ b/0247-tca-tour-pt5/Standups/StandupsTests/StandupsTests.swift @@ -0,0 +1,36 @@ +// +// StandupsTests.swift +// StandupsTests +// +// Created by Brandon Williams on 6/19/23. +// + +import XCTest +@testable import Standups + +final class StandupsTests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() throws { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct results. + // Any test you write for XCTest can be annotated as throws and async. + // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error. + // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards. + } + + func testPerformanceExample() throws { + // This is an example of a performance test case. + self.measure { + // Put the code you want to measure the time of here. + } + } + +} diff --git a/README.md b/README.md index 5df4546f..9d68def8 100644 --- a/README.md +++ b/README.md @@ -248,3 +248,4 @@ This repository is the home of code written on episodes of [Point-Free](https:// 1. [Tour of the Composable Architecture 1.0: Introducing Standups](0244-tca-tour-pt2) 1. [Tour of the Composable Architecture 1.0: Navigation](0245-tca-tour-pt3) 1. [Tour of the Composable Architecture 1.0: Stacks](0246-tca-tour-pt4) +1. [Tour of the Composable Architecture 1.0: Correctness](0247-tca-tour-pt5)