From 5e53f5d723fb485539195b1c51dd34c692b3a331 Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Tue, 10 Aug 2021 12:29:13 -0400 Subject: [PATCH] 156 --- 0155-focus-state/README.md | 2 +- 0156-searchable-pt1/README.md | 5 + .../Search.playground/Contents.swift | 19 + .../Search.playground/contents.xcplayground | 4 + .../Search/Search.xcodeproj/project.pbxproj | 508 ++++++++++++++++++ .../contents.xcworkspacedata | 7 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../AccentColor.colorset/Contents.json | 11 + .../AppIcon.appiconset/Contents.json | 98 ++++ .../Search/Assets.xcassets/Contents.json | 6 + .../Search/Search/ContentView.swift | 229 ++++++++ .../Search/Search/LocalSearchCompleter.swift | 48 ++ .../Preview Assets.xcassets/Contents.json | 6 + .../Search/Search/SearchApp.swift | 21 + .../Search/SearchTests/SearchTests.swift | 7 + README.md | 1 + 16 files changed, 979 insertions(+), 1 deletion(-) create mode 100644 0156-searchable-pt1/README.md create mode 100644 0156-searchable-pt1/Search.playground/Contents.swift create mode 100644 0156-searchable-pt1/Search.playground/contents.xcplayground create mode 100644 0156-searchable-pt1/Search/Search.xcodeproj/project.pbxproj create mode 100644 0156-searchable-pt1/Search/Search.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 0156-searchable-pt1/Search/Search.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 0156-searchable-pt1/Search/Search/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 0156-searchable-pt1/Search/Search/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 0156-searchable-pt1/Search/Search/Assets.xcassets/Contents.json create mode 100644 0156-searchable-pt1/Search/Search/ContentView.swift create mode 100644 0156-searchable-pt1/Search/Search/LocalSearchCompleter.swift create mode 100644 0156-searchable-pt1/Search/Search/Preview Content/Preview Assets.xcassets/Contents.json create mode 100644 0156-searchable-pt1/Search/Search/SearchApp.swift create mode 100644 0156-searchable-pt1/Search/SearchTests/SearchTests.swift diff --git a/0155-focus-state/README.md b/0155-focus-state/README.md index 1ca26018..4b473d5d 100644 --- a/0155-focus-state/README.md +++ b/0155-focus-state/README.md @@ -1,5 +1,5 @@ ## [Point-Free](https://www.pointfree.co) -> #### This directory contains code from Point-Free Episode: [SwiftUI Focus State](https://www.pointfree.co/episodes/ep153-swiftui-focus-state) +> #### This directory contains code from Point-Free Episode: [SwiftUI Focus State](https://www.pointfree.co/episodes/ep155-swiftui-focus-state) > > Let’s explore another API just announced at WWDC: `@FocusState`. We’ll take a simple example and layer on some complexity, including side effects and testability, and we’ll see that the solution we land on works just as well in the Composable Architecture! diff --git a/0156-searchable-pt1/README.md b/0156-searchable-pt1/README.md new file mode 100644 index 00000000..b54e884d --- /dev/null +++ b/0156-searchable-pt1/README.md @@ -0,0 +1,5 @@ +## [Point-Free](https://www.pointfree.co) + +> #### This directory contains code from Point-Free Episode: [SwiftUI Focus State](https://www.pointfree.co/episodes/ep156-swiftui-searchable-part-1) +> +> Let’s develop a new application from scratch to explore SwiftUI’s new `.searchable` API. We’ll use MapKit to search for points of interest, and we will control this complex dependency so that our application can be fully testable. diff --git a/0156-searchable-pt1/Search.playground/Contents.swift b/0156-searchable-pt1/Search.playground/Contents.swift new file mode 100644 index 00000000..55fdbdce --- /dev/null +++ b/0156-searchable-pt1/Search.playground/Contents.swift @@ -0,0 +1,19 @@ +import MapKit + +let completer = MKLocalSearchCompleter() + +class LocalSearchCompleterDelegate: NSObject, MKLocalSearchCompleterDelegate { + func completerDidUpdateResults(_ completer: MKLocalSearchCompleter) { + print("succeeded") + dump(completer.results) + } + + func completer(_ completer: MKLocalSearchCompleter, didFailWithError error: Error) { + print("failed", error) + } +} + +let delegate = LocalSearchCompleterDelegate() +completer.delegate = delegate + +completer.queryFragment = "Apple Store" diff --git a/0156-searchable-pt1/Search.playground/contents.xcplayground b/0156-searchable-pt1/Search.playground/contents.xcplayground new file mode 100644 index 00000000..4a1c4413 --- /dev/null +++ b/0156-searchable-pt1/Search.playground/contents.xcplayground @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/0156-searchable-pt1/Search/Search.xcodeproj/project.pbxproj b/0156-searchable-pt1/Search/Search.xcodeproj/project.pbxproj new file mode 100644 index 00000000..3e801348 --- /dev/null +++ b/0156-searchable-pt1/Search/Search.xcodeproj/project.pbxproj @@ -0,0 +1,508 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 55; + objects = { + +/* Begin PBXBuildFile section */ + 2A7640E326B987060003C3F4 /* SearchApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A7640E226B987060003C3F4 /* SearchApp.swift */; }; + 2A7640E526B987060003C3F4 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A7640E426B987060003C3F4 /* ContentView.swift */; }; + 2A7640E726B987070003C3F4 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2A7640E626B987070003C3F4 /* Assets.xcassets */; }; + 2A7640EA26B987070003C3F4 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2A7640E926B987070003C3F4 /* Preview Assets.xcassets */; }; + 2A7640F426B987070003C3F4 /* SearchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A7640F326B987070003C3F4 /* SearchTests.swift */; }; + 2A926AE226B9970300BAA496 /* LocalSearchCompleter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A926AE126B9970300BAA496 /* LocalSearchCompleter.swift */; }; + 4B83F2D926B98F0A0045BB86 /* ComposableArchitecture in Frameworks */ = {isa = PBXBuildFile; productRef = 4B83F2D826B98F0A0045BB86 /* ComposableArchitecture */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 2A7640F026B987070003C3F4 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 2A7640D726B987060003C3F4 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 2A7640DE26B987060003C3F4; + remoteInfo = Search; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 2A7640DF26B987060003C3F4 /* Search.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Search.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 2A7640E226B987060003C3F4 /* SearchApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchApp.swift; sourceTree = ""; }; + 2A7640E426B987060003C3F4 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; + 2A7640E626B987070003C3F4 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 2A7640E926B987070003C3F4 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; + 2A7640EF26B987070003C3F4 /* SearchTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SearchTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 2A7640F326B987070003C3F4 /* SearchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchTests.swift; sourceTree = ""; }; + 2A76410A26B994970003C3F4 /* Search.playground */ = {isa = PBXFileReference; lastKnownFileType = file.playground; name = Search.playground; path = ../../Search.playground; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.swift; }; + 2A926AE126B9970300BAA496 /* LocalSearchCompleter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalSearchCompleter.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 2A7640DC26B987060003C3F4 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 4B83F2D926B98F0A0045BB86 /* ComposableArchitecture in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 2A7640EC26B987070003C3F4 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 2A7640D626B987060003C3F4 = { + isa = PBXGroup; + children = ( + 2A7640E126B987060003C3F4 /* Search */, + 2A7640F226B987070003C3F4 /* SearchTests */, + 2A7640E026B987060003C3F4 /* Products */, + ); + sourceTree = ""; + }; + 2A7640E026B987060003C3F4 /* Products */ = { + isa = PBXGroup; + children = ( + 2A7640DF26B987060003C3F4 /* Search.app */, + 2A7640EF26B987070003C3F4 /* SearchTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 2A7640E126B987060003C3F4 /* Search */ = { + isa = PBXGroup; + children = ( + 2A76410A26B994970003C3F4 /* Search.playground */, + 2A7640E226B987060003C3F4 /* SearchApp.swift */, + 2A7640E426B987060003C3F4 /* ContentView.swift */, + 2A7640E626B987070003C3F4 /* Assets.xcassets */, + 2A7640E826B987070003C3F4 /* Preview Content */, + 2A926AE126B9970300BAA496 /* LocalSearchCompleter.swift */, + ); + path = Search; + sourceTree = ""; + }; + 2A7640E826B987070003C3F4 /* Preview Content */ = { + isa = PBXGroup; + children = ( + 2A7640E926B987070003C3F4 /* Preview Assets.xcassets */, + ); + path = "Preview Content"; + sourceTree = ""; + }; + 2A7640F226B987070003C3F4 /* SearchTests */ = { + isa = PBXGroup; + children = ( + 2A7640F326B987070003C3F4 /* SearchTests.swift */, + ); + path = SearchTests; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 2A7640DE26B987060003C3F4 /* Search */ = { + isa = PBXNativeTarget; + buildConfigurationList = 2A76410126B987070003C3F4 /* Build configuration list for PBXNativeTarget "Search" */; + buildPhases = ( + 2A7640DB26B987060003C3F4 /* Sources */, + 2A7640DC26B987060003C3F4 /* Frameworks */, + 2A7640DD26B987060003C3F4 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Search; + packageProductDependencies = ( + 4B83F2D826B98F0A0045BB86 /* ComposableArchitecture */, + ); + productName = Search; + productReference = 2A7640DF26B987060003C3F4 /* Search.app */; + productType = "com.apple.product-type.application"; + }; + 2A7640EE26B987070003C3F4 /* SearchTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 2A76410426B987070003C3F4 /* Build configuration list for PBXNativeTarget "SearchTests" */; + buildPhases = ( + 2A7640EB26B987070003C3F4 /* Sources */, + 2A7640EC26B987070003C3F4 /* Frameworks */, + 2A7640ED26B987070003C3F4 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 2A7640F126B987070003C3F4 /* PBXTargetDependency */, + ); + name = SearchTests; + productName = SearchTests; + productReference = 2A7640EF26B987070003C3F4 /* SearchTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 2A7640D726B987060003C3F4 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1300; + LastUpgradeCheck = 1300; + TargetAttributes = { + 2A7640DE26B987060003C3F4 = { + CreatedOnToolsVersion = 13.0; + }; + 2A7640EE26B987070003C3F4 = { + CreatedOnToolsVersion = 13.0; + TestTargetID = 2A7640DE26B987060003C3F4; + }; + }; + }; + buildConfigurationList = 2A7640DA26B987060003C3F4 /* Build configuration list for PBXProject "Search" */; + compatibilityVersion = "Xcode 13.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 2A7640D626B987060003C3F4; + packageReferences = ( + 4B83F2D726B98F0A0045BB86 /* XCRemoteSwiftPackageReference "swift-composable-architecture" */, + ); + productRefGroup = 2A7640E026B987060003C3F4 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 2A7640DE26B987060003C3F4 /* Search */, + 2A7640EE26B987070003C3F4 /* SearchTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 2A7640DD26B987060003C3F4 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 2A7640EA26B987070003C3F4 /* Preview Assets.xcassets in Resources */, + 2A7640E726B987070003C3F4 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 2A7640ED26B987070003C3F4 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 2A7640DB26B987060003C3F4 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 2A7640E526B987060003C3F4 /* ContentView.swift in Sources */, + 2A926AE226B9970300BAA496 /* LocalSearchCompleter.swift in Sources */, + 2A7640E326B987060003C3F4 /* SearchApp.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 2A7640EB26B987070003C3F4 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 2A7640F426B987070003C3F4 /* SearchTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 2A7640F126B987070003C3F4 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 2A7640DE26B987060003C3F4 /* Search */; + targetProxy = 2A7640F026B987070003C3F4 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 2A7640FF26B987070003C3F4 /* 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++17"; + CLANG_CXX_LIBRARY = "libc++"; + 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 = 15.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"; + }; + name = Debug; + }; + 2A76410026B987070003C3F4 /* 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++17"; + CLANG_CXX_LIBRARY = "libc++"; + 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 = 15.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 2A76410226B987070003C3F4 /* 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 = "\"Search/Preview Content\""; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + 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.Search; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 2A76410326B987070003C3F4 /* 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 = "\"Search/Preview Content\""; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + 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.Search; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 2A76410526B987070003C3F4 /* 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; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = co.pointfree.SearchTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Search.app/Search"; + }; + name = Debug; + }; + 2A76410626B987070003C3F4 /* 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; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = co.pointfree.SearchTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Search.app/Search"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 2A7640DA26B987060003C3F4 /* Build configuration list for PBXProject "Search" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 2A7640FF26B987070003C3F4 /* Debug */, + 2A76410026B987070003C3F4 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 2A76410126B987070003C3F4 /* Build configuration list for PBXNativeTarget "Search" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 2A76410226B987070003C3F4 /* Debug */, + 2A76410326B987070003C3F4 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 2A76410426B987070003C3F4 /* Build configuration list for PBXNativeTarget "SearchTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 2A76410526B987070003C3F4 /* Debug */, + 2A76410626B987070003C3F4 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + +/* Begin XCRemoteSwiftPackageReference section */ + 4B83F2D726B98F0A0045BB86 /* XCRemoteSwiftPackageReference "swift-composable-architecture" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/pointfreeco/swift-composable-architecture.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 0.23.0; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 4B83F2D826B98F0A0045BB86 /* ComposableArchitecture */ = { + isa = XCSwiftPackageProductDependency; + package = 4B83F2D726B98F0A0045BB86 /* XCRemoteSwiftPackageReference "swift-composable-architecture" */; + productName = ComposableArchitecture; + }; +/* End XCSwiftPackageProductDependency section */ + }; + rootObject = 2A7640D726B987060003C3F4 /* Project object */; +} diff --git a/0156-searchable-pt1/Search/Search.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/0156-searchable-pt1/Search/Search.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..919434a6 --- /dev/null +++ b/0156-searchable-pt1/Search/Search.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/0156-searchable-pt1/Search/Search.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/0156-searchable-pt1/Search/Search.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/0156-searchable-pt1/Search/Search.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/0156-searchable-pt1/Search/Search/Assets.xcassets/AccentColor.colorset/Contents.json b/0156-searchable-pt1/Search/Search/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 00000000..eb878970 --- /dev/null +++ b/0156-searchable-pt1/Search/Search/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/0156-searchable-pt1/Search/Search/Assets.xcassets/AppIcon.appiconset/Contents.json b/0156-searchable-pt1/Search/Search/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..9221b9bb --- /dev/null +++ b/0156-searchable-pt1/Search/Search/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,98 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/0156-searchable-pt1/Search/Search/Assets.xcassets/Contents.json b/0156-searchable-pt1/Search/Search/Assets.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/0156-searchable-pt1/Search/Search/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/0156-searchable-pt1/Search/Search/ContentView.swift b/0156-searchable-pt1/Search/Search/ContentView.swift new file mode 100644 index 00000000..2960c08d --- /dev/null +++ b/0156-searchable-pt1/Search/Search/ContentView.swift @@ -0,0 +1,229 @@ +import ComposableArchitecture +import MapKit +import SwiftUI + +struct CoordinateRegion: Equatable { + var center = LocationCoordinate2D() + var span = CoordinateSpan() +} + +extension CoordinateRegion { + init(rawValue: MKCoordinateRegion) { + self.init( + center: .init(rawValue: rawValue.center), + span: .init(rawValue: rawValue.span) + ) + } + + var rawValue: MKCoordinateRegion { + .init(center: self.center.rawValue, span: self.span.rawValue) + } +} + +struct LocationCoordinate2D: Equatable { + var latitude: CLLocationDegrees = 0 + var longitude: CLLocationDegrees = 0 +} + +extension LocationCoordinate2D { + init(rawValue: CLLocationCoordinate2D) { + self.init(latitude: rawValue.latitude, longitude: rawValue.longitude) + } + + var rawValue: CLLocationCoordinate2D { + .init(latitude: self.latitude, longitude: self.longitude) + } +} + +struct CoordinateSpan: Equatable { + var latitudeDelta: CLLocationDegrees = 0 + var longitudeDelta: CLLocationDegrees = 0 +} + +extension CoordinateSpan { + init(rawValue: MKCoordinateSpan) { + self.init(latitudeDelta: rawValue.latitudeDelta, longitudeDelta: rawValue.longitudeDelta) + } + + var rawValue: MKCoordinateSpan { + .init(latitudeDelta: self.latitudeDelta, longitudeDelta: self.longitudeDelta) + } +} + + +struct AppState: Equatable { + var completions: [MKLocalSearchCompletion] = [] + var query = "" + var region = CoordinateRegion( + center: .init(latitude: 40.7, longitude: -74), + span: .init(latitudeDelta: 0.075, longitudeDelta: 0.075) + ) +} + +enum AppAction { + case completionsUpdated(Result<[MKLocalSearchCompletion], Error>) + case onAppear + case queryChanged(String) + case regionChanged(CoordinateRegion) +} + +struct AppEnvironment { + var localSearchCompleter: LocalSearchCompleter +} + +let appReducer = Reducer< + AppState, + AppAction, + AppEnvironment +> { state, action, environment in + switch action { + case let .completionsUpdated(.success(completions)): + state.completions = completions + return .none + + case let .completionsUpdated(.failure(error)): + // TODO: error handling + return .none + + case .onAppear: + return environment.localSearchCompleter.completions() + .map(AppAction.completionsUpdated) + + case let .queryChanged(query): + state.query = query + return environment.localSearchCompleter.search(query) + .fireAndForget() + + case let .regionChanged(region): + state.region = region + return .none + } +} + +extension MKLocalSearchCompletion { + var id: [String] { [self.title, self.subtitle] } +} + +struct ContentView: View { + let store: Store + + var body: some View { + WithViewStore(self.store) { viewStore in + Map( + coordinateRegion: viewStore.binding( + get: \.region.rawValue, + send: { .regionChanged(.init(rawValue: $0)) } + ) + // interactionModes: <#T##MapInteractionModes#>, + // showsUserLocation: <#T##Bool#>, + // userTrackingMode: <#T##Binding?#>, + // annotationItems: <#T##RandomAccessCollection#>, + // annotationContent: <#T##(Identifiable) -> MapAnnotationProtocol#> + ) + .searchable( + text: viewStore.binding( + get: \.query, + send: AppAction.queryChanged + ) + // placement: <#T##SearchFieldPlacement#>, + // prompt: <#T##LocalizedStringKey#>, + // suggestions: <#T##() -> View#> + ) { + if viewStore.query.isEmpty { + HStack { + Text("Recent Searches") + Spacer() + Button(action: {}) { + Text("See all") + } + } + .font(.callout) + + HStack { + Image(systemName: "magnifyingglass") + Text("Apple • New York") + Spacer() + } + HStack { + Image(systemName: "magnifyingglass") + Text("Apple • New York") + Spacer() + } + HStack { + Image(systemName: "magnifyingglass") + Text("Apple • New York") + Spacer() + } + + HStack { + Text("Find nearby") + Spacer() + Button(action: {}) { + Text("See all") + } + } + .padding(.top) + .font(.callout) + + ScrollView(.horizontal) { + HStack { + ForEach(1...2, id: \.self) { _ in + VStack { + ForEach(1...2, id: \.self) { _ in + HStack { + Image(systemName: "bag.circle.fill") + .foregroundStyle(Color.white, Color.red) + .font(.title) + Text("Shopping") + } + .padding([.top, .bottom, .trailing], 4) + } + } + } + } + } + + HStack { + Text("Editors’ picks") + Spacer() + Button(action: {}) { + Text("See all") + } + } + .padding(.top) + .font(.callout) + } else { + ForEach(viewStore.completions, id: \.id) { completion in + VStack(alignment: .leading) { + Text(completion.title) + Text(completion.subtitle) + .font(.caption) + } + } + } + } + .navigationTitle("Places") + .navigationBarTitleDisplayMode(.inline) + .ignoresSafeArea(edges: .bottom) + .onAppear { + viewStore.send(.onAppear) + } + } + } +} + +struct ContentView_Previews: PreviewProvider { + static var previews: some View { + NavigationView { + ContentView( + store: .init( + initialState: .init(), + reducer: appReducer, + environment: .init( + localSearchCompleter: .live + ) + ) + ) + } + } +} diff --git a/0156-searchable-pt1/Search/Search/LocalSearchCompleter.swift b/0156-searchable-pt1/Search/Search/LocalSearchCompleter.swift new file mode 100644 index 00000000..7624666c --- /dev/null +++ b/0156-searchable-pt1/Search/Search/LocalSearchCompleter.swift @@ -0,0 +1,48 @@ +import Combine +import ComposableArchitecture +import MapKit + +struct LocalSearchCompleter { + var completions: () -> Effect, Never> + var search: (String) -> Effect +} + +extension LocalSearchCompleter { + static var live: Self { + class Delegate: NSObject, MKLocalSearchCompleterDelegate { + let subscriber: Effect, Never>.Subscriber + + init(subscriber: Effect, Never>.Subscriber) { + self.subscriber = subscriber + } + + func completerDidUpdateResults(_ completer: MKLocalSearchCompleter) { + self.subscriber.send(.success(completer.results)) + } + + func completer(_ completer: MKLocalSearchCompleter, didFailWithError error: Error) { + self.subscriber.send(.failure(error)) + } + } + + let completer = MKLocalSearchCompleter() + + return Self( + completions: { + Effect.run { subscriber in + let delegate = Delegate(subscriber: subscriber) + completer.delegate = delegate + + return AnyCancellable { + _ = delegate + } + } + }, + search: { queryFragment in + .fireAndForget { + completer.queryFragment = queryFragment + } + } + ) + } +} diff --git a/0156-searchable-pt1/Search/Search/Preview Content/Preview Assets.xcassets/Contents.json b/0156-searchable-pt1/Search/Search/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 00000000..73c00596 --- /dev/null +++ b/0156-searchable-pt1/Search/Search/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/0156-searchable-pt1/Search/Search/SearchApp.swift b/0156-searchable-pt1/Search/Search/SearchApp.swift new file mode 100644 index 00000000..d3461c68 --- /dev/null +++ b/0156-searchable-pt1/Search/Search/SearchApp.swift @@ -0,0 +1,21 @@ +import ComposableArchitecture +import SwiftUI + +@main +struct SearchApp: App { + var body: some Scene { + WindowGroup { + NavigationView { + ContentView( + store: .init( + initialState: .init(), + reducer: appReducer.debugActions(), + environment: .init( + localSearchCompleter: .live + ) + ) + ) + } + } + } +} diff --git a/0156-searchable-pt1/Search/SearchTests/SearchTests.swift b/0156-searchable-pt1/Search/SearchTests/SearchTests.swift new file mode 100644 index 00000000..f6bfc064 --- /dev/null +++ b/0156-searchable-pt1/Search/SearchTests/SearchTests.swift @@ -0,0 +1,7 @@ +import XCTest +@testable import Search + +class SearchTests: XCTestCase { + func testExample() throws { + } +} diff --git a/README.md b/README.md index 0d1a0f26..dbd2b9bf 100644 --- a/README.md +++ b/README.md @@ -157,3 +157,4 @@ This repository is the home of code written on episodes of [Point-Free](https:// 1. [Async Refreshable: SwiftUI](0153-refreshable-pt1) 1. [Async Refreshable: Composable Architecture](0154-refreshable-pt2) 1. [SwiftUI Focus State](0155-focus-state) +1. [SwiftUI Searchable: Part 1](0156-searchable-pt1)