diff --git a/0156-searchable-pt1/README.md b/0156-searchable-pt1/README.md
index b54e884d..72dbfbbd 100644
--- a/0156-searchable-pt1/README.md
+++ b/0156-searchable-pt1/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/ep156-swiftui-searchable-part-1)
+> #### This directory contains code from Point-Free Episode: [SwiftUI Searchable: Part 1](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.
+> We finish our search-based application by adding and controlling another MapKit API, integrating it into our application so we can annotate a map with search results, and then we'll go the extra mile and write tests for the entire thing!
diff --git a/0157-searchable-pt2/README.md b/0157-searchable-pt2/README.md
new file mode 100644
index 00000000..102aace0
--- /dev/null
+++ b/0157-searchable-pt2/README.md
@@ -0,0 +1,5 @@
+## [Point-Free](https://www.pointfree.co)
+
+> #### This directory contains code from Point-Free Episode: [SwiftUI Searchable: Part 2](https://www.pointfree.co/episodes/ep157-swiftui-searchable-part-2)
+>
+> 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/0157-searchable-pt2/Search.playground/Contents.swift b/0157-searchable-pt2/Search.playground/Contents.swift
new file mode 100644
index 00000000..a4f782a7
--- /dev/null
+++ b/0157-searchable-pt2/Search.playground/Contents.swift
@@ -0,0 +1,33 @@
+import MapKit
+
+let completer = MKLocalSearchCompleter()
+
+class LocalSearchCompleterDelegate: NSObject, MKLocalSearchCompleterDelegate {
+ func completerDidUpdateResults(_ completer: MKLocalSearchCompleter) {
+ print("succeeded")
+ dump(completer.results)
+
+
+ let search = MKLocalSearch(request: .init(completion: completer.results[0]))
+ Task {
+ let response = try await search.start()
+ print(response.mapItems)
+
+ response.mapItems[0].placemark.coordinate
+ response.boundingRegion
+ }
+
+ }
+
+ func completer(_ completer: MKLocalSearchCompleter, didFailWithError error: Error) {
+ print("failed", error)
+ }
+}
+
+let delegate = LocalSearchCompleterDelegate()
+completer.delegate = delegate
+
+completer.queryFragment = "Apple Store"
+
+//MKLocalSearch.init(request: .init(coordinateRegion: <#T##MKCoordinateRegion#>))
+
diff --git a/0157-searchable-pt2/Search.playground/contents.xcplayground b/0157-searchable-pt2/Search.playground/contents.xcplayground
new file mode 100644
index 00000000..f64b06a8
--- /dev/null
+++ b/0157-searchable-pt2/Search.playground/contents.xcplayground
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/0157-searchable-pt2/Search/Search.xcodeproj/project.pbxproj b/0157-searchable-pt2/Search/Search.xcodeproj/project.pbxproj
new file mode 100644
index 00000000..c31cee66
--- /dev/null
+++ b/0157-searchable-pt2/Search/Search.xcodeproj/project.pbxproj
@@ -0,0 +1,512 @@
+// !$*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 */; };
+ 2A926AE426B9A94500BAA496 /* LocalSearchClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A926AE326B9A94500BAA496 /* LocalSearchClient.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 = ""; };
+ 2A926AE326B9A94500BAA496 /* LocalSearchClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocalSearchClient.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 */,
+ 2A7640E426B987060003C3F4 /* ContentView.swift */,
+ 2A926AE326B9A94500BAA496 /* LocalSearchClient.swift */,
+ 2A926AE126B9970300BAA496 /* LocalSearchCompleter.swift */,
+ 2A7640E226B987060003C3F4 /* SearchApp.swift */,
+ 2A7640E626B987070003C3F4 /* Assets.xcassets */,
+ 2A7640E826B987070003C3F4 /* Preview Content */,
+ );
+ 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 */,
+ 2A926AE426B9A94500BAA496 /* LocalSearchClient.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/0157-searchable-pt2/Search/Search.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/0157-searchable-pt2/Search/Search.xcodeproj/project.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 00000000..919434a6
--- /dev/null
+++ b/0157-searchable-pt2/Search/Search.xcodeproj/project.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/0157-searchable-pt2/Search/Search.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/0157-searchable-pt2/Search/Search.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 00000000..18d98100
--- /dev/null
+++ b/0157-searchable-pt2/Search/Search.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ IDEDidComputeMac32BitWarning
+
+
+
diff --git a/0157-searchable-pt2/Search/Search/Assets.xcassets/AccentColor.colorset/Contents.json b/0157-searchable-pt2/Search/Search/Assets.xcassets/AccentColor.colorset/Contents.json
new file mode 100644
index 00000000..eb878970
--- /dev/null
+++ b/0157-searchable-pt2/Search/Search/Assets.xcassets/AccentColor.colorset/Contents.json
@@ -0,0 +1,11 @@
+{
+ "colors" : [
+ {
+ "idiom" : "universal"
+ }
+ ],
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/0157-searchable-pt2/Search/Search/Assets.xcassets/AppIcon.appiconset/Contents.json b/0157-searchable-pt2/Search/Search/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 00000000..9221b9bb
--- /dev/null
+++ b/0157-searchable-pt2/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/0157-searchable-pt2/Search/Search/Assets.xcassets/Contents.json b/0157-searchable-pt2/Search/Search/Assets.xcassets/Contents.json
new file mode 100644
index 00000000..73c00596
--- /dev/null
+++ b/0157-searchable-pt2/Search/Search/Assets.xcassets/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/0157-searchable-pt2/Search/Search/ContentView.swift b/0157-searchable-pt2/Search/Search/ContentView.swift
new file mode 100644
index 00000000..86fdf54c
--- /dev/null
+++ b/0157-searchable-pt2/Search/Search/ContentView.swift
@@ -0,0 +1,260 @@
+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: [LocalSearchCompletion] = []
+ var mapItems: [MKMapItem] = []
+ var query = ""
+ var region = CoordinateRegion(
+ center: .init(latitude: 40.7, longitude: -74),
+ span: .init(latitudeDelta: 0.075, longitudeDelta: 0.075)
+ )
+}
+
+enum AppAction: Equatable {
+ case completionsUpdated(Result<[LocalSearchCompletion], NSError>)
+ case onAppear
+ case queryChanged(String)
+ case regionChanged(CoordinateRegion)
+ case searchResponse(Result)
+ case tappedCompletion(LocalSearchCompletion)
+}
+
+struct AppEnvironment {
+ var localSearch: LocalSearchClient
+ var localSearchCompleter: LocalSearchCompleter
+ var mainQueue: AnySchedulerOf
+}
+
+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 { $0.mapError { $0 as NSError } }
+ .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
+
+ case let .searchResponse(.success(response)):
+ state.region = response.boundingRegion
+ state.mapItems = response.mapItems
+ return .none
+
+ case let .searchResponse(.failure(error)):
+ // TODO: error handling
+ return .none
+
+ case let .tappedCompletion(completion):
+ state.query = completion.title
+ return environment.localSearch.search(completion)
+ .mapError { $0 as NSError }
+ .receive(on: environment.mainQueue.animation())
+ .catchToEffect()
+ .map(AppAction.searchResponse)
+ }
+}
+
+extension LocalSearchCompletion: Identifiable {
+ var id: [String] { [self.title, self.subtitle] }
+}
+
+extension MKMapItem: Identifiable {}
+
+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: viewStore.mapItems,
+ annotationContent: { mapItem in
+ MapMarker(coordinate: mapItem.placemark.coordinate)
+ }
+ )
+ .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) { completion in
+ Button(action: { viewStore.send(.tappedCompletion(completion)) }) {
+ 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(
+ localSearch: .live,
+ localSearchCompleter: .live,
+ mainQueue: .main
+ )
+ )
+ )
+ }
+ }
+}
diff --git a/0157-searchable-pt2/Search/Search/LocalSearchClient.swift b/0157-searchable-pt2/Search/Search/LocalSearchClient.swift
new file mode 100644
index 00000000..dec583a1
--- /dev/null
+++ b/0157-searchable-pt2/Search/Search/LocalSearchClient.swift
@@ -0,0 +1,51 @@
+import ComposableArchitecture
+import MapKit
+
+struct LocalSearchClient {
+ var search: (LocalSearchCompletion) -> Effect
+
+ struct Response: Equatable {
+ var boundingRegion = CoordinateRegion()
+ var mapItems: [MKMapItem] = []
+ }
+}
+
+extension LocalSearchClient.Response {
+ init(rawValue: MKLocalSearch.Response) {
+ self.boundingRegion = .init(rawValue: rawValue.boundingRegion)
+ self.mapItems = rawValue.mapItems
+ }
+}
+
+extension LocalSearchClient {
+ static let live = Self(
+ search: { completion in
+ .task {
+ .init(
+ rawValue:
+ try await MKLocalSearch(request: .init(completion: completion.rawValue!))
+ .start()
+ )
+ }
+ }
+ )
+}
+
+extension Effect {
+ static func task(
+ priority: TaskPriority? = nil,
+ operation: @escaping @Sendable () async throws -> Output
+ ) -> Self
+ where Failure == Error {
+
+ .future { callback in
+ Task(priority: priority) {
+ do {
+ callback(.success(try await operation()))
+ } catch {
+ callback(.failure(error))
+ }
+ }
+ }
+ }
+}
diff --git a/0157-searchable-pt2/Search/Search/LocalSearchCompleter.swift b/0157-searchable-pt2/Search/Search/LocalSearchCompleter.swift
new file mode 100644
index 00000000..1c724ac9
--- /dev/null
+++ b/0157-searchable-pt2/Search/Search/LocalSearchCompleter.swift
@@ -0,0 +1,77 @@
+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
+ .map(LocalSearchCompletion.init(rawValue:))
+ )
+ )
+ }
+
+ 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
+ }
+ }
+ )
+ }
+}
+
+struct LocalSearchCompletion: Equatable {
+ let rawValue: MKLocalSearchCompletion?
+
+ var subtitle: String
+ var title: String
+
+ init(rawValue: MKLocalSearchCompletion) {
+ self.rawValue = rawValue
+ self.subtitle = rawValue.subtitle
+ self.title = rawValue.title
+ }
+
+ init(subtitle: String, title: String) {
+ self.rawValue = nil
+ self.subtitle = subtitle
+ self.title = title
+ }
+
+ static func == (lhs: Self, rhs: Self) -> Bool {
+ lhs.subtitle == rhs.subtitle
+ && lhs.title == rhs.title
+ }
+}
diff --git a/0157-searchable-pt2/Search/Search/Preview Content/Preview Assets.xcassets/Contents.json b/0157-searchable-pt2/Search/Search/Preview Content/Preview Assets.xcassets/Contents.json
new file mode 100644
index 00000000..73c00596
--- /dev/null
+++ b/0157-searchable-pt2/Search/Search/Preview Content/Preview Assets.xcassets/Contents.json
@@ -0,0 +1,6 @@
+{
+ "info" : {
+ "author" : "xcode",
+ "version" : 1
+ }
+}
diff --git a/0157-searchable-pt2/Search/Search/SearchApp.swift b/0157-searchable-pt2/Search/Search/SearchApp.swift
new file mode 100644
index 00000000..f064b429
--- /dev/null
+++ b/0157-searchable-pt2/Search/Search/SearchApp.swift
@@ -0,0 +1,23 @@
+import ComposableArchitecture
+import SwiftUI
+
+@main
+struct SearchApp: App {
+ var body: some Scene {
+ WindowGroup {
+ NavigationView {
+ ContentView(
+ store: .init(
+ initialState: .init(),
+ reducer: appReducer.debugActions(),
+ environment: .init(
+ localSearch: .live,
+ localSearchCompleter: .live,
+ mainQueue: .main
+ )
+ )
+ )
+ }
+ }
+ }
+}
diff --git a/0157-searchable-pt2/Search/SearchTests/SearchTests.swift b/0157-searchable-pt2/Search/SearchTests/SearchTests.swift
new file mode 100644
index 00000000..8fa9da06
--- /dev/null
+++ b/0157-searchable-pt2/Search/SearchTests/SearchTests.swift
@@ -0,0 +1,81 @@
+import Combine
+import ComposableArchitecture
+import MapKit
+import XCTest
+@testable import Search
+
+class SearchTests: XCTestCase {
+ func testExample() throws {
+ let completions = PassthroughSubject, Never>()
+
+ let store = TestStore(
+ initialState: .init(),
+ reducer: appReducer,
+ environment: .init(
+ localSearch: .failing,
+ localSearchCompleter: .failing,
+ mainQueue: .immediate
+ )
+ )
+
+ store.environment.localSearchCompleter.completions = {
+ completions.eraseToEffect()
+ }
+ let completion = LocalSearchCompletion(
+ subtitle: "Search Nearby",
+ title: "Apple Store"
+ )
+ store.environment.localSearchCompleter.search = { query in
+ .fireAndForget {
+ completions.send(.success([completion]))
+ }
+ }
+ let response = LocalSearchClient.Response(
+ boundingRegion: .init(
+ center: .init(latitude: 0, longitude: 0),
+ span: .init(latitudeDelta: 1, longitudeDelta: 1)
+ ),
+ mapItems: [MKMapItem()]
+ )
+ store.environment.localSearch.search = { _ in
+ .init(value: response)
+ }
+ defer { completions.send(completion: .finished) }
+
+ store.send(.onAppear)
+
+ store.send(.queryChanged("Apple")) {
+ $0.query = "Apple"
+ }
+
+ store.receive(.completionsUpdated(.success([completion]))) {
+ $0.completions = [completion]
+ }
+
+ store.send(.tappedCompletion(completion)) {
+ $0.query = "Apple Store"
+ }
+
+ store.receive(.searchResponse(.success(response))) {
+ $0.region = response.boundingRegion
+ $0.mapItems = response.mapItems
+ }
+ }
+}
+
+extension LocalSearchClient {
+ static let failing = Self(
+ search: { _ in .failing("LocalSearchClient.search is unimplemented") }
+ )
+}
+
+extension LocalSearchCompleter {
+ static let failing = Self(
+ completions: {
+ .failing("LocalSearchCompleter.completions is unimplemented")
+ },
+ search: { _ in
+ .failing("LocalSearchCompleter.search is unimplemented")
+ }
+ )
+}
diff --git a/README.md b/README.md
index dbd2b9bf..53b9d43d 100644
--- a/README.md
+++ b/README.md
@@ -158,3 +158,4 @@ This repository is the home of code written on episodes of [Point-Free](https://
1. [Async Refreshable: Composable Architecture](0154-refreshable-pt2)
1. [SwiftUI Focus State](0155-focus-state)
1. [SwiftUI Searchable: Part 1](0156-searchable-pt1)
+1. [SwiftUI Searchable: Part 2](0157-searchable-pt2)