diff --git a/0040-async-functional-refactoring/PointFreeFramework.xcodeproj/project.pbxproj b/0040-async-functional-refactoring/PointFreeFramework.xcodeproj/project.pbxproj new file mode 100644 index 00000000..bd813671 --- /dev/null +++ b/0040-async-functional-refactoring/PointFreeFramework.xcodeproj/project.pbxproj @@ -0,0 +1,544 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 50; + objects = { + +/* Begin PBXBuildFile section */ + 800BF4D321C4114C009790D3 /* SnapshotTestCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 800BF4CE21C4114C009790D3 /* SnapshotTestCase.swift */; }; + 80B19C0A218AA36400910016 /* PointFreeFramework.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 80B19C00218AA36400910016 /* PointFreeFramework.framework */; }; + 80B19C0F218AA36400910016 /* PointFreeFrameworkTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80B19C0E218AA36400910016 /* PointFreeFrameworkTests.swift */; }; + 80B19C11218AA36400910016 /* PointFreeFramework.h in Headers */ = {isa = PBXBuildFile; fileRef = 80B19C03218AA36400910016 /* PointFreeFramework.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 80B19C1B218AA40200910016 /* Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80B19C1A218AA40200910016 /* Helpers.swift */; }; + 80B19C35218AA47400910016 /* Overture.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 80B19C2C218AA45800910016 /* Overture.framework */; }; + 80B19C41218B3B6900910016 /* EpisodesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80B19C3C218B3B6900910016 /* EpisodesViewController.swift */; }; + 80B19C45218B3B7800910016 /* Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80B19C42218B3B7800910016 /* Utils.swift */; }; + 80B19C46218B3B7800910016 /* Model.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80B19C43218B3B7800910016 /* Model.swift */; }; + 80B19C47218B3B7800910016 /* StyleGuide.swift in Sources */ = {isa = PBXBuildFile; fileRef = 80B19C44218B3B7800910016 /* StyleGuide.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 80B19C0B218AA36400910016 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 80B19BF7218AA36400910016 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 80B19BFF218AA36400910016; + remoteInfo = PointFreeFramework; + }; + 80B19C2B218AA45800910016 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 80B19C24218AA45800910016 /* Overture.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = "Overture::Overture::Product"; + remoteInfo = Overture; + }; + 80B19C2D218AA45800910016 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 80B19C24218AA45800910016 /* Overture.xcodeproj */; + proxyType = 2; + remoteGlobalIDString = "Overture::OvertureTests::Product"; + remoteInfo = OvertureTests; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 800BF4CE21C4114C009790D3 /* SnapshotTestCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SnapshotTestCase.swift; sourceTree = ""; }; + 80B19C00218AA36400910016 /* PointFreeFramework.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = PointFreeFramework.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 80B19C03218AA36400910016 /* PointFreeFramework.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PointFreeFramework.h; sourceTree = ""; }; + 80B19C04218AA36400910016 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 80B19C09218AA36400910016 /* PointFreeFrameworkTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = PointFreeFrameworkTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 80B19C0E218AA36400910016 /* PointFreeFrameworkTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PointFreeFrameworkTests.swift; sourceTree = ""; }; + 80B19C10218AA36400910016 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 80B19C1A218AA40200910016 /* Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Helpers.swift; sourceTree = ""; }; + 80B19C24218AA45800910016 /* Overture.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = Overture.xcodeproj; path = "../0017-styling-pt2/Vendor/swift-overture/Overture.xcodeproj"; sourceTree = ""; }; + 80B19C3C218B3B6900910016 /* EpisodesViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EpisodesViewController.swift; sourceTree = ""; }; + 80B19C42218B3B7800910016 /* Utils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Utils.swift; sourceTree = ""; }; + 80B19C43218B3B7800910016 /* Model.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Model.swift; sourceTree = ""; }; + 80B19C44218B3B7800910016 /* StyleGuide.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StyleGuide.swift; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 80B19BFD218AA36400910016 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 80B19C35218AA47400910016 /* Overture.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 80B19C06218AA36400910016 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 80B19C0A218AA36400910016 /* PointFreeFramework.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 80B19BF6218AA36400910016 = { + isa = PBXGroup; + children = ( + 80B19C02218AA36400910016 /* PointFreeFramework */, + 80B19C0D218AA36400910016 /* PointFreeFrameworkTests */, + 80B19C01218AA36400910016 /* Products */, + 80B19C30218AA47400910016 /* Frameworks */, + 80B19C24218AA45800910016 /* Overture.xcodeproj */, + ); + sourceTree = ""; + }; + 80B19C01218AA36400910016 /* Products */ = { + isa = PBXGroup; + children = ( + 80B19C00218AA36400910016 /* PointFreeFramework.framework */, + 80B19C09218AA36400910016 /* PointFreeFrameworkTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 80B19C02218AA36400910016 /* PointFreeFramework */ = { + isa = PBXGroup; + children = ( + 80B19C03218AA36400910016 /* PointFreeFramework.h */, + 80B19C04218AA36400910016 /* Info.plist */, + 80B19C3C218B3B6900910016 /* EpisodesViewController.swift */, + 80B19C43218B3B7800910016 /* Model.swift */, + 80B19C44218B3B7800910016 /* StyleGuide.swift */, + 80B19C42218B3B7800910016 /* Utils.swift */, + ); + path = PointFreeFramework; + sourceTree = ""; + }; + 80B19C0D218AA36400910016 /* PointFreeFrameworkTests */ = { + isa = PBXGroup; + children = ( + 80B19C1A218AA40200910016 /* Helpers.swift */, + 80B19C0E218AA36400910016 /* PointFreeFrameworkTests.swift */, + 800BF4CE21C4114C009790D3 /* SnapshotTestCase.swift */, + 80B19C10218AA36400910016 /* Info.plist */, + ); + path = PointFreeFrameworkTests; + sourceTree = ""; + }; + 80B19C25218AA45800910016 /* Products */ = { + isa = PBXGroup; + children = ( + 80B19C2C218AA45800910016 /* Overture.framework */, + 80B19C2E218AA45800910016 /* OvertureTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 80B19C30218AA47400910016 /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXHeadersBuildPhase section */ + 80B19BFB218AA36400910016 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + 80B19C11218AA36400910016 /* PointFreeFramework.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXHeadersBuildPhase section */ + +/* Begin PBXNativeTarget section */ + 80B19BFF218AA36400910016 /* PointFreeFramework */ = { + isa = PBXNativeTarget; + buildConfigurationList = 80B19C14218AA36400910016 /* Build configuration list for PBXNativeTarget "PointFreeFramework" */; + buildPhases = ( + 80B19BFB218AA36400910016 /* Headers */, + 80B19BFC218AA36400910016 /* Sources */, + 80B19BFD218AA36400910016 /* Frameworks */, + 80B19BFE218AA36400910016 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = PointFreeFramework; + productName = PointFreeFramework; + productReference = 80B19C00218AA36400910016 /* PointFreeFramework.framework */; + productType = "com.apple.product-type.framework"; + }; + 80B19C08218AA36400910016 /* PointFreeFrameworkTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 80B19C17218AA36400910016 /* Build configuration list for PBXNativeTarget "PointFreeFrameworkTests" */; + buildPhases = ( + 80B19C05218AA36400910016 /* Sources */, + 80B19C06218AA36400910016 /* Frameworks */, + 80B19C07218AA36400910016 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 80B19C0C218AA36400910016 /* PBXTargetDependency */, + ); + name = PointFreeFrameworkTests; + productName = PointFreeFrameworkTests; + productReference = 80B19C09218AA36400910016 /* PointFreeFrameworkTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 80B19BF7218AA36400910016 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 1010; + LastUpgradeCheck = 1010; + ORGANIZATIONNAME = "Point-Free Inc."; + TargetAttributes = { + 80B19BFF218AA36400910016 = { + CreatedOnToolsVersion = 10.1; + LastSwiftMigration = 1010; + }; + 80B19C08218AA36400910016 = { + CreatedOnToolsVersion = 10.1; + }; + }; + }; + buildConfigurationList = 80B19BFA218AA36400910016 /* Build configuration list for PBXProject "PointFreeFramework" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + ); + mainGroup = 80B19BF6218AA36400910016; + productRefGroup = 80B19C01218AA36400910016 /* Products */; + projectDirPath = ""; + projectReferences = ( + { + ProductGroup = 80B19C25218AA45800910016 /* Products */; + ProjectRef = 80B19C24218AA45800910016 /* Overture.xcodeproj */; + }, + ); + projectRoot = ""; + targets = ( + 80B19BFF218AA36400910016 /* PointFreeFramework */, + 80B19C08218AA36400910016 /* PointFreeFrameworkTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXReferenceProxy section */ + 80B19C2C218AA45800910016 /* Overture.framework */ = { + isa = PBXReferenceProxy; + fileType = wrapper.framework; + path = Overture.framework; + remoteRef = 80B19C2B218AA45800910016 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; + 80B19C2E218AA45800910016 /* OvertureTests.xctest */ = { + isa = PBXReferenceProxy; + fileType = file; + path = OvertureTests.xctest; + remoteRef = 80B19C2D218AA45800910016 /* PBXContainerItemProxy */; + sourceTree = BUILT_PRODUCTS_DIR; + }; +/* End PBXReferenceProxy section */ + +/* Begin PBXResourcesBuildPhase section */ + 80B19BFE218AA36400910016 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 80B19C07218AA36400910016 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 80B19BFC218AA36400910016 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 80B19C47218B3B7800910016 /* StyleGuide.swift in Sources */, + 80B19C45218B3B7800910016 /* Utils.swift in Sources */, + 80B19C41218B3B6900910016 /* EpisodesViewController.swift in Sources */, + 80B19C46218B3B7800910016 /* Model.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 80B19C05218AA36400910016 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 80B19C0F218AA36400910016 /* PointFreeFrameworkTests.swift in Sources */, + 800BF4D321C4114C009790D3 /* SnapshotTestCase.swift in Sources */, + 80B19C1B218AA40200910016 /* Helpers.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 80B19C0C218AA36400910016 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 80B19BFF218AA36400910016 /* PointFreeFramework */; + targetProxy = 80B19C0B218AA36400910016 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 80B19C12218AA36400910016 /* 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++14"; + 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_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; + CODE_SIGN_IDENTITY = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + 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 = 12.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"; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Debug; + }; + 80B19C13218AA36400910016 /* 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++14"; + 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_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; + CODE_SIGN_IDENTITY = "iPhone Developer"; + COPY_PHASE_STRIP = NO; + CURRENT_PROJECT_VERSION = 1; + 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 = 12.1; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VALIDATE_PRODUCT = YES; + VERSIONING_SYSTEM = "apple-generic"; + VERSION_INFO_PREFIX = ""; + }; + name = Release; + }; + 80B19C15218AA36400910016 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = PointFreeFramework/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = co.pointfree.PointFreeFramework; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 4.2; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 80B19C16218AA36400910016 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_STYLE = Automatic; + DEFINES_MODULE = YES; + DYLIB_COMPATIBILITY_VERSION = 1; + DYLIB_CURRENT_VERSION = 1; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + INFOPLIST_FILE = PointFreeFramework/Info.plist; + INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = co.pointfree.PointFreeFramework; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SWIFT_VERSION = 4.2; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; + 80B19C18218AA36400910016 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = PointFreeFrameworkTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = co.pointfree.PointFreeFrameworkTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 4.2; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 80B19C19218AA36400910016 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES; + CODE_SIGN_STYLE = Automatic; + INFOPLIST_FILE = PointFreeFrameworkTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + "@loader_path/Frameworks", + ); + PRODUCT_BUNDLE_IDENTIFIER = co.pointfree.PointFreeFrameworkTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 4.2; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 80B19BFA218AA36400910016 /* Build configuration list for PBXProject "PointFreeFramework" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 80B19C12218AA36400910016 /* Debug */, + 80B19C13218AA36400910016 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 80B19C14218AA36400910016 /* Build configuration list for PBXNativeTarget "PointFreeFramework" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 80B19C15218AA36400910016 /* Debug */, + 80B19C16218AA36400910016 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 80B19C17218AA36400910016 /* Build configuration list for PBXNativeTarget "PointFreeFrameworkTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 80B19C18218AA36400910016 /* Debug */, + 80B19C19218AA36400910016 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 80B19BF7218AA36400910016 /* Project object */; +} diff --git a/0040-async-functional-refactoring/PointFreeFramework.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/0040-async-functional-refactoring/PointFreeFramework.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..47823c02 --- /dev/null +++ b/0040-async-functional-refactoring/PointFreeFramework.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/0040-async-functional-refactoring/PointFreeFramework.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/0040-async-functional-refactoring/PointFreeFramework.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/0040-async-functional-refactoring/PointFreeFramework.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/0040-async-functional-refactoring/PointFreeFramework/EpisodesViewController.swift b/0040-async-functional-refactoring/PointFreeFramework/EpisodesViewController.swift new file mode 100644 index 00000000..8c0ddb3c --- /dev/null +++ b/0040-async-functional-refactoring/PointFreeFramework/EpisodesViewController.swift @@ -0,0 +1,200 @@ +import Foundation +import UIKit +import Overture + +final class SubscribeCalloutCell: UITableViewCell { + private let bodyLabel = UILabel() + private let buttonsStackView = UIStackView() + private let cardView = UIView() + private let loginButton = UIButton() + private let orLabel = UILabel() + private let rootStackView = UIStackView() + private let subscribeButton = UIButton() + private let titleLabel = UILabel() + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + + self.selectionStyle = .none + self.contentView.layoutMargins = .init(top: .pf_grid(6), left: .pf_grid(6), bottom: .pf_grid(6), right: .pf_grid(6)) + + self.titleLabel.text = "Subscribe to Point-Free" + self.titleLabel.font = UIFont.preferredFont(forTextStyle: .title3) + + self.bodyLabel.text = "👋 Hey there! See anything you like? You may be interested in subscribing so that you get access to these episodes and all future ones." + self.bodyLabel.numberOfLines = 0 + self.bodyLabel.font = UIFont.preferredFont(forTextStyle: UIFont.TextStyle.subheadline) + + self.cardView.backgroundColor = UIColor(white: 0.96, alpha: 1.0) + with(self.cardView, generousMargins) + self.cardView.translatesAutoresizingMaskIntoConstraints = false + self.contentView.addSubview(self.cardView) + + self.rootStackView.alignment = .leading + with(self.rootStackView, baseStackViewStyle) + self.rootStackView.addArrangedSubview(self.titleLabel) + self.rootStackView.addArrangedSubview(self.bodyLabel) + self.rootStackView.addArrangedSubview(self.buttonsStackView) + self.contentView.addSubview(self.rootStackView) + + self.orLabel.text = "or" + self.orLabel.font = UIFont.preferredFont(forTextStyle: .subheadline) + + self.subscribeButton.setTitle("See subscription options", for: .normal) + with(self.subscribeButton, primaryButtonStyle) + + self.loginButton.setTitle("Login", for: .normal) + with(self.loginButton, secondaryTextButtonStyle) + + self.buttonsStackView.spacing = .pf_grid(2) + self.buttonsStackView.alignment = .firstBaseline + self.buttonsStackView.addArrangedSubview(self.subscribeButton) + self.buttonsStackView.addArrangedSubview(self.orLabel) + self.buttonsStackView.addArrangedSubview(self.loginButton) + + NSLayoutConstraint.activate([ + self.rootStackView.leadingAnchor.constraint(equalTo: self.contentView.layoutMarginsGuide.leadingAnchor), + self.rootStackView.topAnchor.constraint(equalTo: self.contentView.layoutMarginsGuide.topAnchor), + self.rootStackView.trailingAnchor.constraint(equalTo: self.contentView.layoutMarginsGuide.trailingAnchor), + self.rootStackView.bottomAnchor.constraint(equalTo: self.contentView.layoutMarginsGuide.bottomAnchor), + + self.cardView.leadingAnchor.constraint(equalTo: self.rootStackView.leadingAnchor), + self.cardView.topAnchor.constraint(equalTo: self.rootStackView.topAnchor), + self.cardView.trailingAnchor.constraint(equalTo: self.rootStackView.trailingAnchor), + self.cardView.bottomAnchor.constraint(equalTo: self.rootStackView.bottomAnchor), + ]) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + +final class EpisodeCell: UITableViewCell { + private let blurbLabel = UILabel() + private let contentStackView = UIStackView() + private let posterImageView = UIImageView() + private let rootStackView = UIStackView() + private let sequenceAndDateLabel = UILabel() + private let subscriberOnlyLabel = UILabel() + private lazy var subscriberOnlyLabelWrapper = with( + wrapView( + padding: UIEdgeInsets( + top: .pf_grid(1), + left: .pf_grid(2), + bottom: .pf_grid(1), + right: .pf_grid(2) + ) + )(self.subscriberOnlyLabel), + concat( + autoLayoutStyle, + baseRoundedStyle, + mut(\UIView.backgroundColor, UIColor(white: 0, alpha: 0.3)) + ) + ) + + private let titleLabel = UILabel() + private let watchNowButton = UIButton() + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + + self.blurbLabel.numberOfLines = 0 + self.blurbLabel.font = UIFont.preferredFont(forTextStyle: .subheadline) + + with(self.contentStackView, concat( + baseStackViewStyle, + mut(\.layoutMargins.bottom, .pf_grid(8)) + )) + self.contentStackView.alignment = .leading + self.contentStackView.addArrangedSubview(self.sequenceAndDateLabel) + self.contentStackView.addArrangedSubview(self.titleLabel) + self.contentStackView.addArrangedSubview(self.blurbLabel) + self.contentStackView.addArrangedSubview(self.watchNowButton) + + with(self.rootStackView, concat( + autoLayoutStyle, + verticalStackView + )) + self.rootStackView.addArrangedSubview(self.posterImageView) + self.rootStackView.addArrangedSubview(self.contentStackView) + + with(self.sequenceAndDateLabel, smallCapsLabelStyle) + + self.titleLabel.font = UIFont.preferredFont(forTextStyle: .title2) + + self.watchNowButton.setTitle("Watch episode →", for: .normal) + with(self.watchNowButton, primaryTextButtonStyle) + + self.subscriberOnlyLabel.text = "Subscriber Only" + with(self.subscriberOnlyLabel, concat( + smallCapsLabelStyle, + mut(\.textColor, .white) + )) + + self.contentView.addSubview(self.rootStackView) +// self.contentView.addSubview(self.subscriberOnlyLabelWrapper) + + NSLayoutConstraint.activate([ + self.rootStackView.leadingAnchor.constraint(equalTo: self.contentView.leadingAnchor), + self.rootStackView.trailingAnchor.constraint(equalTo: self.contentView.trailingAnchor), + self.rootStackView.topAnchor.constraint(equalTo: self.contentView.topAnchor), + self.rootStackView.bottomAnchor.constraint(equalTo: self.contentView.bottomAnchor), + + self.posterImageView.widthAnchor.constraint(equalTo: self.posterImageView.heightAnchor, multiplier: 16/9), + +// self.subscriberOnlyLabelWrapper.topAnchor.constraint(equalTo: self.posterImageView.topAnchor, constant: .pf_grid(3)), +// self.subscriberOnlyLabelWrapper.trailingAnchor.constraint(equalTo: self.posterImageView.trailingAnchor, constant: -.pf_grid(6)), + ]) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + func configure(with episode: Episode) { + self.posterImageView.backgroundColor = episode.color + self.titleLabel.text = episode.title + self.blurbLabel.text = episode.blurb + let formattedDate = episodeDateFormatter.string(from: episode.publishedAt) + self.sequenceAndDateLabel.text = "#\(episode.sequence) • \(formattedDate)" + self.subscriberOnlyLabelWrapper.isHidden = !episode.subscriberOnly + + URLSession.shared.dataTask(with: URL(string: episode.posterImageUrl)!) { data, _, _ in + DispatchQueue.main.async { self.posterImageView.image = data.flatMap(UIImage.init(data:)) } + }.resume() + } +} + +public final class EpisodeListViewController: UITableViewController { + let episodes: [Episode] + + init(episodes: Episodes) where Episodes.Element == Episode { + self.episodes = Array(episodes) + super.init(nibName: nil, bundle: nil) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + override public func viewDidLoad() { + super.viewDidLoad() + self.tableView.estimatedRowHeight = 400 + self.tableView.rowHeight = UITableView.automaticDimension + } + + override public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + if indexPath.row == 0 { + return SubscribeCalloutCell(style: .default, reuseIdentifier: nil) + } + + let cell = EpisodeCell(style: .default, reuseIdentifier: nil) + cell.configure(with: self.episodes[indexPath.row - 1]) + return cell + } + + override public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return self.episodes.count + 1 + } +} diff --git a/0040-async-functional-refactoring/PointFreeFramework/Info.plist b/0040-async-functional-refactoring/PointFreeFramework/Info.plist new file mode 100644 index 00000000..e1fe4cfb --- /dev/null +++ b/0040-async-functional-refactoring/PointFreeFramework/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + + diff --git a/0040-async-functional-refactoring/PointFreeFramework/Model.swift b/0040-async-functional-refactoring/PointFreeFramework/Model.swift new file mode 100644 index 00000000..2b3e7d8f --- /dev/null +++ b/0040-async-functional-refactoring/PointFreeFramework/Model.swift @@ -0,0 +1,70 @@ +import Foundation +import UIKit + +public struct Episode { + public let blurb: String + public let color: UIColor + public let posterImageUrl: String + public let publishedAt: Date + public let sequence: Int + public let subscriberOnly: Bool + public let title: String +} + +public let episodes: [Episode] = [ +// .init( +// blurb: """ +//What does the Swift type system have to do with algebra? A lot! We’ll begin to explore this correspondence and see how it can help us create type-safe data structures that can catch runtime errors at compile time. +//""", +// color: .pf_yellow, +// posterImageUrl: "https://d1hf1soyumxcgv.cloudfront.net/0004-adt/0004-poster.jpg", +// publishedAt: Date(timeIntervalSince1970: 1_519_045_951), +// sequence: 4, +// subscriberOnly: true, +// title: "Algebraic Data Types" +// ), + .init( + blurb: """ +We bring tools from previous episodes down to earth and apply them to an everyday task: UIKit styling. Plain functions unlock worlds of composability and reusability in styling of UI components. Have we finally solved the styling problem? +""", + color: .pf_purple, + posterImageUrl: "https://d1hf1soyumxcgv.cloudfront.net/0003-styling-with-functions/0003-poster.jpg", + publishedAt: Date(timeIntervalSince1970: 1_519_045_951), + sequence: 3, + subscriberOnly: false, + title: "UIKit Styling with Functions" + ), + .init( + blurb: """ +Side effects: can’t live with ’em; can’t write a program without ’em. Let’s explore a few kinds of side effects we encounter every day, why they make code difficult to reason about and test, and how we can control them without losing composition. +""", + color: .pf_blue, + posterImageUrl: "https://d1hf1soyumxcgv.cloudfront.net/0002-side-effects/0002-poster.jpg", + publishedAt: Date(timeIntervalSince1970: 1_517_811_069), + sequence: 2, + subscriberOnly: false, + title: "Side Effects" + ), + .init( + blurb: """ +Our first episode is all about functions! We talk a bit about what makes functions special, contrasting them with the way we usually write code, and have some exploratory discussions about operators and composition. +""", + color: .pf_green, + posterImageUrl: "https://d1hf1soyumxcgv.cloudfront.net/0001-functions/0001-poster.jpg", + publishedAt: Date(timeIntervalSince1970: 1_517_206_269), + sequence: 1, + subscriberOnly: false, + title: "Functions" + ), + .init( + blurb: """ +Point-Free is here, bringing you videos covering functional programming concepts using the Swift language. Take a moment to hear from the hosts about what to expect from this new series. +""", + color: .pf_yellow, + posterImageUrl: "https://d1hf1soyumxcgv.cloudfront.net/0000-introduction/0000-poster.jpg", + publishedAt: Date(timeIntervalSince1970: 1_517_206_269), + sequence: 0, + subscriberOnly: false, + title: "We launched!" + ), +] diff --git a/0040-async-functional-refactoring/PointFreeFramework/PointFreeFramework.h b/0040-async-functional-refactoring/PointFreeFramework/PointFreeFramework.h new file mode 100644 index 00000000..23ee84a8 --- /dev/null +++ b/0040-async-functional-refactoring/PointFreeFramework/PointFreeFramework.h @@ -0,0 +1,3 @@ +#import +FOUNDATION_EXPORT double PointFreeFrameworkVersionNumber; +FOUNDATION_EXPORT const unsigned char PointFreeFrameworkVersionString[]; diff --git a/0040-async-functional-refactoring/PointFreeFramework/StyleGuide.swift b/0040-async-functional-refactoring/PointFreeFramework/StyleGuide.swift new file mode 100644 index 00000000..40cbb065 --- /dev/null +++ b/0040-async-functional-refactoring/PointFreeFramework/StyleGuide.swift @@ -0,0 +1,82 @@ +import Foundation +import UIKit +import Overture + +extension CGFloat { + static func pf_grid(_ n: Int) -> CGFloat { + return CGFloat(n) * 4 + } +} + +let generousMargins = + mut(\UIView.layoutMargins, .init(top: .pf_grid(6), left: .pf_grid(6), bottom: .pf_grid(6), right: .pf_grid(6))) + +let autoLayoutStyle = mut(\UIView.translatesAutoresizingMaskIntoConstraints, false) + +let verticalStackView = mut(\UIStackView.axis, .vertical) + +let baseStackViewStyle = concat( + generousMargins, + mut(\UIStackView.spacing, .pf_grid(3)), + verticalStackView, + mut(\.isLayoutMarginsRelativeArrangement, true), + autoLayoutStyle +) + +let bolded: (inout UIFont) -> Void = { $0 = $0.bolded } + +let baseTextButtonStyle = concat( + mut(\UIButton.titleLabel!.font, UIFont.preferredFont(forTextStyle: .subheadline)), + mver(\UIButton.titleLabel!.font!, bolded) +) + +extension UIButton { + var normalTitleColor: UIColor? { + get { return self.titleColor(for: .normal) } + set { self.setTitleColor(newValue, for: .normal) } + } +} + +let secondaryTextButtonStyle = concat( + baseTextButtonStyle, + mut(\.normalTitleColor, .black) +) + +let primaryTextButtonStyle = concat( + baseTextButtonStyle, + mut(\.normalTitleColor, .pf_purple) +) + +let baseButtonStyle = concat( + baseTextButtonStyle, + mut(\.contentEdgeInsets, .init(top: .pf_grid(2), left: .pf_grid(4), bottom: .pf_grid(2), right: .pf_grid(4))) +) + +func roundedStyle(cornerRadius: CGFloat) -> (UIView) -> Void { + return concat( + mut(\.layer.cornerRadius, cornerRadius), + mut(\.layer.masksToBounds, true) + ) +} + +let baseRoundedStyle = roundedStyle(cornerRadius: 6) + +let baseFilledButtonStyle = concat( + baseButtonStyle, + baseRoundedStyle +) + +extension UIButton { + var normalBackgroundImage: UIImage? { + get { return self.backgroundImage(for: .normal) } + set { self.setBackgroundImage(newValue, for: .normal) } + } +} + +let primaryButtonStyle = concat( + baseFilledButtonStyle, + mut(\.normalBackgroundImage, .from(color: .pf_purple)), + mut(\.normalTitleColor, .white) +) + +let smallCapsLabelStyle = mut(\UILabel.font, UIFont.preferredFont(forTextStyle: .caption1).smallCaps) diff --git a/0040-async-functional-refactoring/PointFreeFramework/Utils.swift b/0040-async-functional-refactoring/PointFreeFramework/Utils.swift new file mode 100644 index 00000000..5e102ee3 --- /dev/null +++ b/0040-async-functional-refactoring/PointFreeFramework/Utils.swift @@ -0,0 +1,75 @@ +import Foundation +import UIKit + +public var episodeDateFormatter: DateFormatter { + let formatter = DateFormatter() + formatter.dateFormat = "EEEE MMM d, yyyy" + formatter.timeZone = TimeZone(secondsFromGMT: 0) + return formatter +} + +extension UIColor { + public static let pf_black = UIColor(white: 0.07, alpha: 1) + public static let pf_blue = UIColor(red: 76/255, green: 204/255, blue: 255/255, alpha: 1) + public static let pf_gray950 = UIColor(white: 0.95, alpha: 1.0) + public static let pf_green = UIColor(red: 121/255, green: 242/255, blue: 176/255, alpha: 1) + public static let pf_purple = UIColor(red: 151/255, green: 77/255, blue: 255/255, alpha: 1) + public static let pf_red = UIColor(red: 235/255, green: 28/255, blue: 38/255, alpha: 1) + public static let pf_yellow = UIColor(red: 255/255, green: 240/255, blue: 128/255, alpha: 1) +} + +extension UIImage { + public static func from(color: UIColor) -> UIImage { + let rect = CGRect(x: 0, y: 0, width: 1, height: 1) + UIGraphicsBeginImageContext(rect.size) + defer { UIGraphicsEndImageContext() } + guard let context = UIGraphicsGetCurrentContext() else { return UIImage() } + context.setFillColor(color.cgColor) + context.fill(rect) + return UIGraphicsGetImageFromCurrentImageContext() ?? UIImage() + } +} + +extension UIFont { + public var smallCaps: UIFont { + let upperCaseFeature = [ + UIFontDescriptor.FeatureKey.featureIdentifier : kUpperCaseType, + UIFontDescriptor.FeatureKey.typeIdentifier : kUpperCaseSmallCapsSelector + ] + let lowerCaseFeature = [ + UIFontDescriptor.FeatureKey.featureIdentifier : kLowerCaseType, + UIFontDescriptor.FeatureKey.typeIdentifier : kLowerCaseSmallCapsSelector + ] + let features = [upperCaseFeature, lowerCaseFeature] + let smallCapsDescriptor = self.fontDescriptor.addingAttributes([UIFontDescriptor.AttributeName.featureSettings : features]) + return UIFont(descriptor: smallCapsDescriptor, size: 0) + } + + public var bolded: UIFont { + guard let descriptor = self.fontDescriptor.withSymbolicTraits(.traitBold) else { return self } + return UIFont(descriptor: descriptor, size: 0) + } +} + +func wrapView(padding: UIEdgeInsets) -> (UIView) -> UIView { + return { subview in + let wrapper = UIView() + subview.translatesAutoresizingMaskIntoConstraints = false + wrapper.addSubview(subview) + NSLayoutConstraint.activate([ + subview.leadingAnchor.constraint( + equalTo: wrapper.leadingAnchor, constant: padding.left + ), + subview.rightAnchor.constraint( + equalTo: wrapper.rightAnchor, constant: -padding.right + ), + subview.topAnchor.constraint( + equalTo: wrapper.topAnchor, constant: padding.top + ), + subview.bottomAnchor.constraint( + equalTo: wrapper.bottomAnchor, constant: -padding.bottom + ), + ]) + return wrapper + } +} diff --git a/0040-async-functional-refactoring/PointFreeFrameworkTests/Helpers.swift b/0040-async-functional-refactoring/PointFreeFrameworkTests/Helpers.swift new file mode 100644 index 00000000..857bfe89 --- /dev/null +++ b/0040-async-functional-refactoring/PointFreeFrameworkTests/Helpers.swift @@ -0,0 +1,221 @@ +import UIKit + +func snapshotUrl(file: StaticString, function: String) -> URL { + return snapshotDirectoryUrl(file: file) + .appendingPathComponent(String(function.dropLast(2))) +} + +func snapshotDirectoryUrl(file: StaticString) -> URL { + let fileUrl = URL(fileURLWithPath: "\(file)") + let directoryUrl = fileUrl + .deletingLastPathComponent() + .appendingPathComponent("__Snapshots__") + .appendingPathComponent(fileUrl.deletingPathExtension().lastPathComponent) + try! FileManager.default.createDirectory(at: directoryUrl, withIntermediateDirectories: true) + return directoryUrl +} + +enum Diff { + static func lines(_ old: String, _ new: String) -> String? { + if old == new { return nil } + let hunks = chunk(diff: diff( + old.split(separator: "\n", omittingEmptySubsequences: false).map(String.init), + new.split(separator: "\n", omittingEmptySubsequences: false).map(String.init) + )) + return hunks.flatMap { [$0.patchMark] + $0.lines }.joined(separator: "\n") + } + + static func images(_ old: UIImage, _ new: UIImage, precision: Float = 1) -> UIImage? { + if compare(old, new, precision: precision) { return nil } + return diff(old, new) + } +} + +// MARK: - Private + +private struct Difference { + enum Which { + case first + case second + case both + } + + let elements: [A] + let which: Which +} + +private func diff(_ fst: [A], _ snd: [A]) -> [Difference] { + var idxsOf = [A: [Int]]() + fst.enumerated().forEach { idxsOf[$1, default: []].append($0) } + + let sub = snd.enumerated().reduce((overlap: [Int: Int](), fst: 0, snd: 0, len: 0)) { sub, sndPair in + (idxsOf[sndPair.element] ?? []) + .reduce((overlap: [Int: Int](), fst: sub.fst, snd: sub.snd, len: sub.len)) { innerSub, fstIdx in + + var newOverlap = innerSub.overlap + newOverlap[fstIdx] = (sub.overlap[fstIdx - 1] ?? 0) + 1 + + if let newLen = newOverlap[fstIdx], newLen > sub.len { + return (newOverlap, fstIdx - newLen + 1, sndPair.offset - newLen + 1, newLen) + } + return (newOverlap, innerSub.fst, innerSub.snd, innerSub.len) + } + } + let (_, fstIdx, sndIdx, len) = sub + + if len == 0 { + let fstDiff = fst.isEmpty ? [] : [Difference(elements: fst, which: .first)] + let sndDiff = snd.isEmpty ? [] : [Difference(elements: snd, which: .second)] + return fstDiff + sndDiff + } else { + let fstDiff = diff(Array(fst.prefix(upTo: fstIdx)), Array(snd.prefix(upTo: sndIdx))) + let midDiff = [Difference(elements: Array(fst.suffix(from: fstIdx).prefix(len)), which: .both)] + let lstDiff = diff(Array(fst.suffix(from: fstIdx + len)), Array(snd.suffix(from: sndIdx + len))) + return fstDiff + midDiff + lstDiff + } +} + +private let minus = "−" +private let plus = "+" +private let figureSpace = "\u{2007}" + +private struct Hunk { + let fstIdx: Int + let fstLen: Int + let sndIdx: Int + let sndLen: Int + let lines: [String] + + var patchMark: String { + let fstMark = "\(minus)\(fstIdx + 1),\(fstLen)" + let sndMark = "\(plus)\(sndIdx + 1),\(sndLen)" + return "@@ \(fstMark) \(sndMark) @@" + } + + static func +(lhs: Hunk, rhs: Hunk) -> Hunk { + return Hunk( + fstIdx: lhs.fstIdx + rhs.fstIdx, + fstLen: lhs.fstLen + rhs.fstLen, + sndIdx: lhs.sndIdx + rhs.sndIdx, + sndLen: lhs.sndLen + rhs.sndLen, + lines: lhs.lines + rhs.lines + ) + } + + init(fstIdx: Int = 0, fstLen: Int = 0, sndIdx: Int = 0, sndLen: Int = 0, lines: [String] = []) { + self.fstIdx = fstIdx + self.fstLen = fstLen + self.sndIdx = sndIdx + self.sndLen = sndLen + self.lines = lines + } + + public init(idx: Int = 0, len: Int = 0, lines: [String] = []) { + self.init(fstIdx: idx, fstLen: len, sndIdx: idx, sndLen: len, lines: lines) + } +} + +private func chunk(diff diffs: [Difference], context ctx: Int = 4) -> [Hunk] { + func prepending(_ prefix: String) -> (String) -> String { + return { prefix + $0 + ($0.hasSuffix(" ") ? "¬" : "") } + } + let changed: (Hunk) -> Bool = { $0.lines.contains(where: { $0.hasPrefix(minus) || $0.hasPrefix(plus) }) } + + let (hunk, hunks) = diffs + .reduce((current: Hunk(), hunks: [Hunk]())) { cursor, diff in + let (current, hunks) = cursor + let len = diff.elements.count + + switch diff.which { + case .both where len > ctx * 2: + let hunk = current + Hunk(len: ctx, lines: diff.elements.prefix(ctx).map(prepending(figureSpace))) + let next = Hunk( + fstIdx: current.fstIdx + current.fstLen + len - ctx, + fstLen: ctx, + sndIdx: current.sndIdx + current.sndLen + len - ctx, + sndLen: ctx, + lines: (diff.elements.suffix(ctx) as ArraySlice).map(prepending(figureSpace)) + ) + return (next, changed(hunk) ? hunks + [hunk] : hunks) + case .both where current.lines.isEmpty: + let lines = (diff.elements.suffix(ctx) as ArraySlice).map(prepending(figureSpace)) + let count = lines.count + return (current + Hunk(idx: len - count, len: count, lines: lines), hunks) + case .both: + return (current + Hunk(len: len, lines: diff.elements.map(prepending(figureSpace))), hunks) + case .first: + return (current + Hunk(fstLen: len, lines: diff.elements.map(prepending(minus))), hunks) + case .second: + return (current + Hunk(sndLen: len, lines: diff.elements.map(prepending(plus))), hunks) + } + } + + return changed(hunk) ? hunks + [hunk] : hunks +} + +private func compare(_ old: UIImage, _ new: UIImage, precision: Float) -> Bool { + guard let oldCgImage = old.cgImage else { return false } + guard let newCgImage = new.cgImage else { return false } + guard oldCgImage.width != 0 else { return false } + guard newCgImage.width != 0 else { return false } + guard oldCgImage.width == newCgImage.width else { return false } + guard oldCgImage.height != 0 else { return false } + guard newCgImage.height != 0 else { return false } + guard oldCgImage.height == newCgImage.height else { return false } + let byteCount = oldCgImage.width * oldCgImage.height * 4 + var oldBytes = [UInt8](repeating: 0, count: byteCount) + guard let oldContext = context(for: oldCgImage, data: &oldBytes) else { return false } + guard let newContext = context(for: newCgImage) else { return false } + guard let oldData = oldContext.data else { return false } + guard let newData = newContext.data else { return false } + if memcmp(oldData, newData, byteCount) == 0 { return true } + let newer = UIImage(data: new.pngData()!)! + guard let newerCgImage = newer.cgImage else { return false } + var newerBytes = [UInt8](repeating: 0, count: byteCount) + guard let newerContext = context(for: newerCgImage, data: &newerBytes) else { return false } + guard let newerData = newerContext.data else { return false } + if memcmp(oldData, newerData, byteCount) == 0 { return true } + if precision >= 1 { return false } + var differentPixelCount = 0 + let threshold = 1 - precision + for x in 0.. threshold { return false} + } + } + return true +} + +private func context(for cgImage: CGImage, data: UnsafeMutableRawPointer? = nil) -> CGContext? { + guard + let space = cgImage.colorSpace, + let context = CGContext( + data: data, + width: cgImage.width, + height: cgImage.height, + bitsPerComponent: cgImage.bitsPerComponent, + bytesPerRow: cgImage.bytesPerRow, + space: space, + bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue + ) + else { return nil } + + context.draw(cgImage, in: CGRect(x: 0, y: 0, width: cgImage.width, height: cgImage.height)) + return context +} + +private func diff(_ old: UIImage, _ new: UIImage) -> UIImage { + let oldCiImage = CIImage(cgImage: old.cgImage!) + let newCiImage = CIImage(cgImage: new.cgImage!) + let differenceFilter = CIFilter(name: "CIDifferenceBlendMode")! + differenceFilter.setValue(oldCiImage, forKey: kCIInputImageKey) + differenceFilter.setValue(newCiImage, forKey: kCIInputBackgroundImageKey) + let differenceCiImage = differenceFilter.outputImage! + let invertFilter = CIFilter(name: "CIColorInvert")! + invertFilter.setValue(differenceCiImage, forKey: kCIInputImageKey) + let invertCiImage = invertFilter.outputImage! + let context = CIContext() + let invertCgImage = context.createCGImage(invertCiImage, from: invertCiImage.extent)! + return UIImage(cgImage: invertCgImage) +} diff --git a/0040-async-functional-refactoring/PointFreeFrameworkTests/Info.plist b/0040-async-functional-refactoring/PointFreeFrameworkTests/Info.plist new file mode 100644 index 00000000..6c40a6cd --- /dev/null +++ b/0040-async-functional-refactoring/PointFreeFrameworkTests/Info.plist @@ -0,0 +1,22 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + + diff --git a/0040-async-functional-refactoring/PointFreeFrameworkTests/PointFreeFrameworkTests.swift b/0040-async-functional-refactoring/PointFreeFrameworkTests/PointFreeFrameworkTests.swift new file mode 100644 index 00000000..6fb412e7 --- /dev/null +++ b/0040-async-functional-refactoring/PointFreeFrameworkTests/PointFreeFrameworkTests.swift @@ -0,0 +1,65 @@ +import XCTest +import WebKit +@testable import PointFreeFramework + +class PointFreeFrameworkTests: SnapshotTestCase { + func testWebView() { + let html = """ +

Welcome to Point-Free!

+

A Swift video series exploring functional programming and more.

+""" + + let webView = WKWebView(frame: CGRect(x: 0, y: 0, width: 640, height: 480)) + webView.loadHTMLString(html, baseURL: nil) + assertSnapshot(matching: webView, as: .image) + +// record = true +// let loaded = expectation(description: "loaded") +// let delegate = NavigationDelegate.init(callback: { +// webView.takeSnapshot(with: nil) { (image, error) in +// loaded.fulfill() +// self.assertSnapshot(matching: image!, as: .image) +// } +// }) +// webView.navigationDelegate = delegate +// wait(for: [loaded], timeout: 5) + } +} + +class NavigationDelegate: NSObject, WKNavigationDelegate { + var callback: (() -> Void)? + + func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { + self.callback!() + } +} + +extension Snapshotting where A == WKWebView, Snapshot == UIImage { +// static let image: Snapshotting = Snapshotting.image.pullback { webView in + static let image: Snapshotting = Snapshotting.image.asyncPullback { webView in + return Parallel { callback in + let delegate = NavigationDelegate() + delegate.callback = { + webView.takeSnapshot(with: nil) { (image, error) in + callback(image!) + _ = delegate + } + } + webView.navigationDelegate = delegate + } + } +// static let image = Snapshotting.init( +// diffing: .image, +// pathExtension: "png") { webView -> Parallel in +// return Parallel { callback in +// let delegate = NavigationDelegate() +// delegate.callback = { +// webView.takeSnapshot(with: nil) { (image, error) in +// callback(image!) +// _ = delegate +// } +// } +// webView.navigationDelegate = delegate +// } +// } +} diff --git a/0040-async-functional-refactoring/PointFreeFrameworkTests/SnapshotTestCase.swift b/0040-async-functional-refactoring/PointFreeFrameworkTests/SnapshotTestCase.swift new file mode 100644 index 00000000..f9735e02 --- /dev/null +++ b/0040-async-functional-refactoring/PointFreeFrameworkTests/SnapshotTestCase.swift @@ -0,0 +1,168 @@ +import Overture +import XCTest + +struct Diffing
{ + let diff: (A, A) -> (String, [XCTAttachment])? + let from: (Data) -> A + let data: (A) -> Data +} + +struct Parallel { + let run: (@escaping (A) -> Void) -> Void +} + +struct Snapshotting { + let diffing: Diffing + let pathExtension: String + let snapshot: (A) -> Parallel + + func pullback(_ f: @escaping (A0) -> A) -> Snapshotting { + return Snapshotting.init( + diffing: self.diffing, + pathExtension: self.pathExtension, + snapshot: { a0 in self.snapshot(f(a0)) } + ) + } + + func asyncPullback(_ f: @escaping (A0) -> Parallel) -> Snapshotting { + return Snapshotting( + diffing: self.diffing, + pathExtension: self.pathExtension, + snapshot: { (a0) -> Parallel in + return Parallel { callback in +// callback // (Snapshot) -> Void +// a0 // A0 +// f // (A0) -> Parallel +// self.snapshot // (A) -> Parallel + let parallelA = f(a0) + parallelA.run { a in + let parallelSnapshot = self.snapshot(a) + parallelSnapshot.run { snapshot in + callback(snapshot) + } + } + } + } + ) + } +} + +extension Snapshotting { + init(diffing: Diffing, pathExtension: String, snapshot: @escaping (A) -> Snapshot) { + self.diffing = diffing + self.pathExtension = pathExtension + self.snapshot = { a in Parallel { callback in callback(snapshot(a)) } } + } +} + +extension Diffing where A == String { + static let lines = Diffing( + diff: { old, new in + guard let string = Diff.lines(old, new) else { return nil } + return ("Difference:\n\n\(string)", [XCTAttachment(string: string)]) + }, + from: { data in + return String(decoding: data, as: UTF8.self) + }, + data: { string in + return Data(string.utf8) + } + ) +} + +extension Diffing where A == UIImage { + static let image = Diffing( + diff: { old, new in + guard let difference = Diff.images(old, new) else { return nil } + return ( + "Expected old@\(old.size) to match new@\(new.size)", + [old, new, difference].map(XCTAttachment.init) + ) + }, + from: { data in UIImage(data: data, scale: UIScreen.main.scale)! }, + data: { image in image.pngData()! } + ) +} + +extension Snapshotting where A == UIImage, Snapshot == UIImage { + static let image = Snapshotting( + diffing: .image, + pathExtension: "png", + snapshot: { $0 } + ) +} + +extension Snapshotting where A == CALayer, Snapshot == UIImage { + static let image: Snapshotting = Snapshotting.image.pullback { layer in + UIGraphicsImageRenderer(size: layer.bounds.size) + .image { ctx in layer.render(in: ctx.cgContext) } + } +} + +extension Snapshotting where A == UIView, Snapshot == UIImage { + static let image: Snapshotting = Snapshotting.image.pullback(get(\.layer)) +} + +extension Snapshotting where A == UIViewController, Snapshot == UIImage { + static let image: Snapshotting = Snapshotting.image.pullback(get(\.view)) +} + +extension Snapshotting where A == String, Snapshot == String { + static let lines = Snapshotting( + diffing: .lines, + pathExtension: "txt", + snapshot: { $0 } + ) +} + +extension Snapshotting where A == UIView, Snapshot == String { + static let recursiveDescription: Snapshotting = Snapshotting.lines.pullback { view in + view.setNeedsLayout() + view.layoutIfNeeded() + + return (view.perform(Selector(("recursiveDescription")))? + .takeUnretainedValue() as! String) + .replacingOccurrences(of: ":?\\s*0x[\\da-f]+(\\s*)", with: "$1", options: .regularExpression) + } +} + +extension Snapshotting where A == UIViewController, Snapshot == String { + static let recursiveDescription: Snapshotting = Snapshotting.recursiveDescription.pullback(get(\.view)) +} + +class SnapshotTestCase: XCTestCase { + var record = false + + func assertSnapshot( + matching value: A, + as witness: Snapshotting, + file: StaticString = #file, + function: String = #function, + line: UInt = #line) { + + let parallel = witness.snapshot(value) + var snapshot: Snapshot! + let loaded = expectation(description: "Loaded") + parallel.run { + snapshot = $0 + loaded.fulfill() + } + wait(for: [loaded], timeout: 5) + + let referenceUrl = snapshotUrl(file: file, function: function) + .appendingPathExtension(witness.pathExtension) + + if !self.record, let referenceData = try? Data(contentsOf: referenceUrl) { + let reference = witness.diffing.from(referenceData) + guard let (failure, attachments) = witness.diffing.diff(reference, snapshot) else { return } + XCTFail(failure, file: file, line: line) + XCTContext.runActivity(named: "Attached failure diff") { activity in + attachments + .forEach { image in activity.add(image) } + } + } else { + try! witness.diffing.data(snapshot).write(to: referenceUrl) + XCTFail("Recorded: …\n\"\(referenceUrl.path)\"", file: file, line: line) + } + } +} diff --git a/0040-async-functional-refactoring/PointFreeFrameworkTests/__Snapshots__/PointFreeFrameworkTests/testEpisodesView.png b/0040-async-functional-refactoring/PointFreeFrameworkTests/__Snapshots__/PointFreeFrameworkTests/testEpisodesView.png new file mode 100644 index 00000000..68bddca7 Binary files /dev/null and b/0040-async-functional-refactoring/PointFreeFrameworkTests/__Snapshots__/PointFreeFrameworkTests/testEpisodesView.png differ diff --git a/0040-async-functional-refactoring/PointFreeFrameworkTests/__Snapshots__/PointFreeFrameworkTests/testEpisodesView.txt b/0040-async-functional-refactoring/PointFreeFrameworkTests/__Snapshots__/PointFreeFrameworkTests/testEpisodesView.txt new file mode 100644 index 00000000..c0c1cc2e --- /dev/null +++ b/0040-async-functional-refactoring/PointFreeFrameworkTests/__Snapshots__/PointFreeFrameworkTests/testEpisodesView.txt @@ -0,0 +1,42 @@ +; layer = ; contentOffset: {0, 0}; contentSize: {414, 2025.5}; adjustedContentInset: {0, 0, 0, 0}> + | > + | | ; layer = > + | | | > + | | | | > + | | | | > + | | | | | > + | | | | | > + | | | | | > + | | | | | > + | | | | | | > + | | |