diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..e0bf90a --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,23 @@ +name: Build + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + build: + runs-on: macos-latest + steps: + - uses: actions/checkout@v2 + - name: Build + run: xcodebuild -project SearchTextfield.xcodeproj -target SearchTextField -destination 'platform=iOS Simulator,name=iPhone 11,OS=13.5' + # - name: Run tests + # run: swift test -v + swiftlint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: SwiftLint + uses: norio-nomura/action-swiftlint@3.1.0 diff --git a/.gitignore b/.gitignore index 9bce6af..1dcb724 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ xcuserdata +.build/ +**/build/* \ No newline at end of file diff --git a/.swiftformat b/.swiftformat new file mode 100644 index 0000000..a6a1ff7 --- /dev/null +++ b/.swiftformat @@ -0,0 +1,7 @@ +# format options +--indent tab +--tabwidth 4 + +# file options +--exclude .Build +--exclude Example \ No newline at end of file diff --git a/.swiftlint.yml b/.swiftlint.yml new file mode 100644 index 0000000..a329f26 --- /dev/null +++ b/.swiftlint.yml @@ -0,0 +1,6 @@ +disabled_rules: + - trailing_comma +excluded: + - .build + - Example + - Tests diff --git a/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata b/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/Package.swift b/Package.swift new file mode 100644 index 0000000..547e427 --- /dev/null +++ b/Package.swift @@ -0,0 +1,29 @@ +// swift-tools-version:5.2 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "SearchTextField", + platforms: [.iOS(.v9)], + products: [ + // Products define the executables and libraries produced by a package, and make them visible to other packages. + .library( + name: "SearchTextField", + targets: ["SearchTextField"] + ), + ], + dependencies: [], + targets: [ + // Targets are the basic building blocks of a package. A target can define a module or a test suite. + // Targets can depend on other targets in this package, and on products in packages which this package depends on. + .target( + name: "SearchTextField", + dependencies: [] + ), + .testTarget( + name: "SearchTextFieldTests", + dependencies: ["SearchTextField"] + ), + ] +) diff --git a/README.md b/README.md index 409e9d9..0989802 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,7 @@ # SearchTextField +![Build](https://github.com/eatsleepride/SearchTextField/workflows/Build/badge.svg) [![Version](https://img.shields.io/cocoapods/v/SearchTextField.svg?style=flat)](http://cocoapods.org/pods/SearchTextField) [![License](https://img.shields.io/cocoapods/l/SearchTextField.svg?style=flat)](http://cocoapods.org/pods/SearchTextField) [![Platform](https://img.shields.io/cocoapods/p/SearchTextField.svg?style=flat)](http://cocoapods.org/pods/SearchTextField) @@ -24,7 +25,14 @@ Now you can make suggestions "inline", showing the first matched result as the p ## Installation -SearchTextField is available through [CocoaPods](http://cocoapods.org). To install +SearchTextField is available through [SwiftPM](https://swift.org/package-manager/) + +To install via SPM: +1. xcode -> file -> Swift Packages -> Add Package Dependency +2. search for SearchTextField or use the git clone url. +3. select the branch, version, or commit you want to use. + +SearchTextField is also available through [CocoaPods](http://cocoapods.org). To install it, simply add the following line to your Podfile: ```swift diff --git a/SearchTextField.podspec b/SearchTextField.podspec index 0c01f23..ef30567 100644 --- a/SearchTextField.podspec +++ b/SearchTextField.podspec @@ -8,7 +8,7 @@ Pod::Spec.new do |s| s.name = "SearchTextField" - s.version = "1.2.4" + s.version = "1.2.5" s.summary = "SearchTextField extends UITextField allowing you to add the autocomplete feature in a really easy way" s.swift_version = "5.0" @@ -37,7 +37,7 @@ SearchTextField supports two different modes: the classic dropdown list (by defa s.user_target_xcconfig = { 'ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES' => '$(inherited)' } - s.source_files = 'SearchTextField/Classes/**/*' + s.source_files = 'Sources/SearchTextField/Classes/**/*' #s.resource_bundles = { # 'SearchTextField' => ['SearchTextField/Assets/*.png'] #} diff --git a/SearchTextField.xcodeproj/SearchTextFieldTests_Info.plist b/SearchTextField.xcodeproj/SearchTextFieldTests_Info.plist new file mode 100644 index 0000000..7c23420 --- /dev/null +++ b/SearchTextField.xcodeproj/SearchTextFieldTests_Info.plist @@ -0,0 +1,25 @@ + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + BNDL + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + NSPrincipalClass + + + diff --git a/SearchTextField.xcodeproj/SearchTextField_Info.plist b/SearchTextField.xcodeproj/SearchTextField_Info.plist new file mode 100644 index 0000000..57ada9f --- /dev/null +++ b/SearchTextField.xcodeproj/SearchTextField_Info.plist @@ -0,0 +1,25 @@ + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleSignature + ???? + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + NSPrincipalClass + + + diff --git a/SearchTextField.xcodeproj/project.pbxproj b/SearchTextField.xcodeproj/project.pbxproj new file mode 100644 index 0000000..34172c0 --- /dev/null +++ b/SearchTextField.xcodeproj/project.pbxproj @@ -0,0 +1,508 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXAggregateTarget section */ + "SearchTextField::SearchTextFieldPackageTests::ProductTarget" /* SearchTextFieldPackageTests */ = { + isa = PBXAggregateTarget; + buildConfigurationList = OBJ_35 /* Build configuration list for PBXAggregateTarget "SearchTextFieldPackageTests" */; + buildPhases = ( + ); + dependencies = ( + OBJ_38 /* PBXTargetDependency */, + ); + name = SearchTextFieldPackageTests; + productName = SearchTextFieldPackageTests; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + OBJ_26 /* SearchTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_10 /* SearchTextField.swift */; }; + OBJ_33 /* Package.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_6 /* Package.swift */; }; + OBJ_44 /* SearchTextFieldTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_13 /* SearchTextFieldTest.swift */; }; + OBJ_46 /* SearchTextField.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = "SearchTextField::SearchTextField::Product" /* SearchTextField.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 84DCF4CD24773E2400CF9185 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = OBJ_1 /* Project object */; + proxyType = 1; + remoteGlobalIDString = "SearchTextField::SearchTextField"; + remoteInfo = SearchTextField; + }; + 84DCF4CE24773E2C00CF9185 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = OBJ_1 /* Project object */; + proxyType = 1; + remoteGlobalIDString = "SearchTextField::SearchTextFieldTests"; + remoteInfo = SearchTextFieldTests; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + OBJ_10 /* SearchTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchTextField.swift; sourceTree = ""; }; + OBJ_13 /* SearchTextFieldTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchTextFieldTest.swift; sourceTree = ""; }; + OBJ_17 /* Example */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Example; sourceTree = SOURCE_ROOT; }; + OBJ_18 /* LICENSE */ = {isa = PBXFileReference; lastKnownFileType = text; path = LICENSE; sourceTree = ""; }; + OBJ_19 /* SearchTextField.podspec */ = {isa = PBXFileReference; lastKnownFileType = text; path = SearchTextField.podspec; sourceTree = ""; }; + OBJ_20 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; + OBJ_6 /* Package.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; path = Package.swift; sourceTree = ""; }; + "SearchTextField::SearchTextField::Product" /* SearchTextField.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = SearchTextField.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + "SearchTextField::SearchTextFieldTests::Product" /* SearchTextFieldTests.xctest */ = {isa = PBXFileReference; lastKnownFileType = file; path = SearchTextFieldTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + OBJ_27 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 0; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + OBJ_45 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 0; + files = ( + OBJ_46 /* SearchTextField.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + OBJ_11 /* Tests */ = { + isa = PBXGroup; + children = ( + OBJ_12 /* SearchTextFieldTests */, + ); + name = Tests; + sourceTree = SOURCE_ROOT; + }; + OBJ_12 /* SearchTextFieldTests */ = { + isa = PBXGroup; + children = ( + OBJ_13 /* SearchTextFieldTest.swift */, + ); + name = SearchTextFieldTests; + path = Tests/SearchTextFieldTests; + sourceTree = SOURCE_ROOT; + }; + OBJ_14 /* Products */ = { + isa = PBXGroup; + children = ( + "SearchTextField::SearchTextFieldTests::Product" /* SearchTextFieldTests.xctest */, + "SearchTextField::SearchTextField::Product" /* SearchTextField.framework */, + ); + name = Products; + sourceTree = BUILT_PRODUCTS_DIR; + }; + OBJ_5 /* */ = { + isa = PBXGroup; + children = ( + OBJ_6 /* Package.swift */, + OBJ_7 /* Sources */, + OBJ_11 /* Tests */, + OBJ_14 /* Products */, + OBJ_17 /* Example */, + OBJ_18 /* LICENSE */, + OBJ_19 /* SearchTextField.podspec */, + OBJ_20 /* README.md */, + ); + name = ""; + sourceTree = ""; + }; + OBJ_7 /* Sources */ = { + isa = PBXGroup; + children = ( + OBJ_8 /* SearchTextField */, + ); + name = Sources; + sourceTree = SOURCE_ROOT; + }; + OBJ_8 /* SearchTextField */ = { + isa = PBXGroup; + children = ( + OBJ_9 /* Classes */, + ); + name = SearchTextField; + path = Sources/SearchTextField; + sourceTree = SOURCE_ROOT; + }; + OBJ_9 /* Classes */ = { + isa = PBXGroup; + children = ( + OBJ_10 /* SearchTextField.swift */, + ); + path = Classes; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + "SearchTextField::SearchTextField" /* SearchTextField */ = { + isa = PBXNativeTarget; + buildConfigurationList = OBJ_22 /* Build configuration list for PBXNativeTarget "SearchTextField" */; + buildPhases = ( + OBJ_25 /* Sources */, + OBJ_27 /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = SearchTextField; + productName = SearchTextField; + productReference = "SearchTextField::SearchTextField::Product" /* SearchTextField.framework */; + productType = "com.apple.product-type.framework"; + }; + "SearchTextField::SearchTextFieldTests" /* SearchTextFieldTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = OBJ_40 /* Build configuration list for PBXNativeTarget "SearchTextFieldTests" */; + buildPhases = ( + OBJ_43 /* Sources */, + OBJ_45 /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + OBJ_47 /* PBXTargetDependency */, + ); + name = SearchTextFieldTests; + productName = SearchTextFieldTests; + productReference = "SearchTextField::SearchTextFieldTests::Product" /* SearchTextFieldTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + "SearchTextField::SwiftPMPackageDescription" /* SearchTextFieldPackageDescription */ = { + isa = PBXNativeTarget; + buildConfigurationList = OBJ_29 /* Build configuration list for PBXNativeTarget "SearchTextFieldPackageDescription" */; + buildPhases = ( + OBJ_32 /* Sources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = SearchTextFieldPackageDescription; + productName = SearchTextFieldPackageDescription; + productType = "com.apple.product-type.framework"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + OBJ_1 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftMigration = 9999; + LastUpgradeCheck = 9999; + }; + buildConfigurationList = OBJ_2 /* Build configuration list for PBXProject "SearchTextField" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + ); + mainGroup = OBJ_5 /* */; + productRefGroup = OBJ_14 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + "SearchTextField::SearchTextField" /* SearchTextField */, + "SearchTextField::SwiftPMPackageDescription" /* SearchTextFieldPackageDescription */, + "SearchTextField::SearchTextFieldPackageTests::ProductTarget" /* SearchTextFieldPackageTests */, + "SearchTextField::SearchTextFieldTests" /* SearchTextFieldTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXSourcesBuildPhase section */ + OBJ_25 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 0; + files = ( + OBJ_26 /* SearchTextField.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + OBJ_32 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 0; + files = ( + OBJ_33 /* Package.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + OBJ_43 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 0; + files = ( + OBJ_44 /* SearchTextFieldTest.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + OBJ_38 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = "SearchTextField::SearchTextFieldTests" /* SearchTextFieldTests */; + targetProxy = 84DCF4CE24773E2C00CF9185 /* PBXContainerItemProxy */; + }; + OBJ_47 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = "SearchTextField::SearchTextField" /* SearchTextField */; + targetProxy = 84DCF4CD24773E2400CF9185 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + OBJ_23 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ENABLE_TESTABILITY = YES; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PLATFORM_DIR)/Developer/Library/Frameworks", + ); + HEADER_SEARCH_PATHS = "$(inherited)"; + INFOPLIST_FILE = SearchTextField.xcodeproj/SearchTextField_Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) $(TOOLCHAIN_DIR)/usr/lib/swift/macosx"; + MACOSX_DEPLOYMENT_TARGET = 10.10; + OTHER_CFLAGS = "$(inherited)"; + OTHER_LDFLAGS = "$(inherited)"; + OTHER_SWIFT_FLAGS = "$(inherited)"; + PRODUCT_BUNDLE_IDENTIFIER = SearchTextField; + PRODUCT_MODULE_NAME = "$(TARGET_NAME:c99extidentifier)"; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited)"; + SWIFT_VERSION = 5.0; + TARGET_NAME = SearchTextField; + TVOS_DEPLOYMENT_TARGET = 9.0; + WATCHOS_DEPLOYMENT_TARGET = 2.0; + }; + name = Debug; + }; + OBJ_24 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ENABLE_TESTABILITY = YES; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PLATFORM_DIR)/Developer/Library/Frameworks", + ); + HEADER_SEARCH_PATHS = "$(inherited)"; + INFOPLIST_FILE = SearchTextField.xcodeproj/SearchTextField_Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) $(TOOLCHAIN_DIR)/usr/lib/swift/macosx"; + MACOSX_DEPLOYMENT_TARGET = 10.10; + ONLY_ACTIVE_ARCH = YES; + OTHER_CFLAGS = "$(inherited)"; + OTHER_LDFLAGS = "$(inherited)"; + OTHER_SWIFT_FLAGS = "$(inherited)"; + PRODUCT_BUNDLE_IDENTIFIER = SearchTextField; + PRODUCT_MODULE_NAME = "$(TARGET_NAME:c99extidentifier)"; + PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; + SKIP_INSTALL = YES; + SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited)"; + SWIFT_VERSION = 5.0; + TARGET_NAME = SearchTextField; + TVOS_DEPLOYMENT_TARGET = 9.0; + WATCHOS_DEPLOYMENT_TARGET = 2.0; + }; + name = Release; + }; + OBJ_3 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_OBJC_ARC = YES; + COMBINE_HIDPI_IMAGES = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + ENABLE_NS_ASSERTIONS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "$(inherited)", + "SWIFT_PACKAGE=1", + "DEBUG=1", + ); + MACOSX_DEPLOYMENT_TARGET = 10.10; + ONLY_ACTIVE_ARCH = NO; + OTHER_SWIFT_FLAGS = "$(inherited) -DXcode"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) SWIFT_PACKAGE DEBUG"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + USE_HEADERMAP = NO; + }; + name = Debug; + }; + OBJ_30 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + LD = /usr/bin/true; + OTHER_SWIFT_FLAGS = "-swift-version 5 -I $(TOOLCHAIN_DIR)/usr/lib/swift/pm/4_2 -target x86_64-apple-macosx10.10 -sdk /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.15.sdk -package-description-version 5.2.0"; + SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + OBJ_31 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + LD = /usr/bin/true; + OTHER_SWIFT_FLAGS = "-swift-version 5 -I $(TOOLCHAIN_DIR)/usr/lib/swift/pm/4_2 -target x86_64-apple-macosx10.10 -sdk /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.15.sdk -package-description-version 5.2.0"; + SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + OBJ_36 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + }; + name = Debug; + }; + OBJ_37 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + }; + name = Release; + }; + OBJ_4 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_OBJC_ARC = YES; + COMBINE_HIDPI_IMAGES = YES; + COPY_PHASE_STRIP = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DYLIB_INSTALL_NAME_BASE = "@rpath"; + GCC_OPTIMIZATION_LEVEL = s; + GCC_PREPROCESSOR_DEFINITIONS = ( + "$(inherited)", + "SWIFT_PACKAGE=1", + ); + MACOSX_DEPLOYMENT_TARGET = 10.10; + ONLY_ACTIVE_ARCH = NO; + OTHER_SWIFT_FLAGS = "$(inherited) -DXcode"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SDKROOT = iphoneos; + SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) SWIFT_PACKAGE"; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + USE_HEADERMAP = NO; + }; + name = Release; + }; + OBJ_41 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + EMBEDDED_CONTENT_CONTAINS_SWIFT = YES; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PLATFORM_DIR)/Developer/Library/Frameworks", + ); + HEADER_SEARCH_PATHS = "$(inherited)"; + INFOPLIST_FILE = SearchTextField.xcodeproj/SearchTextFieldTests_Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @loader_path/../Frameworks @loader_path/Frameworks"; + MACOSX_DEPLOYMENT_TARGET = 10.10; + OTHER_CFLAGS = "$(inherited)"; + OTHER_LDFLAGS = "$(inherited)"; + OTHER_SWIFT_FLAGS = "$(inherited)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited)"; + SWIFT_VERSION = 5.0; + TARGET_NAME = SearchTextFieldTests; + TVOS_DEPLOYMENT_TARGET = 9.0; + WATCHOS_DEPLOYMENT_TARGET = 2.0; + }; + name = Debug; + }; + OBJ_42 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CLANG_ENABLE_MODULES = YES; + EMBEDDED_CONTENT_CONTAINS_SWIFT = YES; + FRAMEWORK_SEARCH_PATHS = ( + "$(inherited)", + "$(PLATFORM_DIR)/Developer/Library/Frameworks", + ); + HEADER_SEARCH_PATHS = "$(inherited)"; + INFOPLIST_FILE = SearchTextField.xcodeproj/SearchTextFieldTests_Info.plist; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @loader_path/../Frameworks @loader_path/Frameworks"; + MACOSX_DEPLOYMENT_TARGET = 10.10; + OTHER_CFLAGS = "$(inherited)"; + OTHER_LDFLAGS = "$(inherited)"; + OTHER_SWIFT_FLAGS = "$(inherited)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited)"; + SWIFT_VERSION = 5.0; + TARGET_NAME = SearchTextFieldTests; + TVOS_DEPLOYMENT_TARGET = 9.0; + WATCHOS_DEPLOYMENT_TARGET = 2.0; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + OBJ_2 /* Build configuration list for PBXProject "SearchTextField" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + OBJ_3 /* Debug */, + OBJ_4 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + OBJ_22 /* Build configuration list for PBXNativeTarget "SearchTextField" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + OBJ_23 /* Debug */, + OBJ_24 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + OBJ_29 /* Build configuration list for PBXNativeTarget "SearchTextFieldPackageDescription" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + OBJ_30 /* Debug */, + OBJ_31 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + OBJ_35 /* Build configuration list for PBXAggregateTarget "SearchTextFieldPackageTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + OBJ_36 /* Debug */, + OBJ_37 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + OBJ_40 /* Build configuration list for PBXNativeTarget "SearchTextFieldTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + OBJ_41 /* Debug */, + OBJ_42 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = OBJ_1 /* Project object */; +} diff --git a/SearchTextField.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/SearchTextField.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..fe1aa71 --- /dev/null +++ b/SearchTextField.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/SearchTextField.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/SearchTextField.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/SearchTextField.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/SearchTextField.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/SearchTextField.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..a72dc2b --- /dev/null +++ b/SearchTextField.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + IDEWorkspaceSharedSettings_AutocreateContextsIfNeeded + + + \ No newline at end of file diff --git a/SearchTextField.xcodeproj/xcshareddata/xcschemes/SearchTextField-Package.xcscheme b/SearchTextField.xcodeproj/xcshareddata/xcschemes/SearchTextField-Package.xcscheme new file mode 100644 index 0000000..5f0371c --- /dev/null +++ b/SearchTextField.xcodeproj/xcshareddata/xcschemes/SearchTextField-Package.xcscheme @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/SearchTextField/Classes/.gitkeep b/SearchTextField/Classes/.gitkeep deleted file mode 100755 index e69de29..0000000 diff --git a/SearchTextField/Classes/SearchTextField.swift b/SearchTextField/Classes/SearchTextField.swift deleted file mode 100755 index 9cf8f55..0000000 --- a/SearchTextField/Classes/SearchTextField.swift +++ /dev/null @@ -1,680 +0,0 @@ -// -// SearchTextField.swift -// SearchTextField -// -// Created by Alejandro Pasccon on 4/20/16. -// Copyright © 2016 Alejandro Pasccon. All rights reserved. -// - -import UIKit - -open class SearchTextField: UITextField { - - //////////////////////////////////////////////////////////////////////// - // Public interface - - /// Maximum number of results to be shown in the suggestions list - open var maxNumberOfResults = 0 - - /// Maximum height of the results list - open var maxResultsListHeight = 0 - - /// Indicate if this field has been interacted with yet - open var interactedWith = false - - /// Indicate if keyboard is showing or not - open var keyboardIsShowing = false - - /// How long to wait before deciding typing has stopped - open var typingStoppedDelay = 0.8 - - /// Set your custom visual theme, or just choose between pre-defined SearchTextFieldTheme.lightTheme() and SearchTextFieldTheme.darkTheme() themes - open var theme = SearchTextFieldTheme.lightTheme() { - didSet { - tableView?.reloadData() - - if let placeholderColor = theme.placeholderColor { - if let placeholderString = placeholder { - self.attributedPlaceholder = NSAttributedString(string: placeholderString, attributes: [NSAttributedString.Key.foregroundColor: placeholderColor]) - } - - self.placeholderLabel?.textColor = placeholderColor - } - - if let hightlightedFont = self.highlightAttributes[.font] as? UIFont { - self.highlightAttributes[.font] = hightlightedFont.withSize(self.theme.font.pointSize) - } - } - } - - /// Show the suggestions list without filter when the text field is focused - open var startVisible = false - - /// Show the suggestions list without filter even if the text field is not focused - open var startVisibleWithoutInteraction = false { - didSet { - if startVisibleWithoutInteraction { - textFieldDidChange() - } - } - } - - /// Set an array of SearchTextFieldItem's to be used for suggestions - open func filterItems(_ items: [SearchTextFieldItem]) { - filterDataSource = items - } - - /// Set an array of strings to be used for suggestions - open func filterStrings(_ strings: [String]) { - var items = [SearchTextFieldItem]() - - for value in strings { - items.append(SearchTextFieldItem(title: value)) - } - - filterItems(items) - } - - /// Closure to handle when the user pick an item - open var itemSelectionHandler: SearchTextFieldItemHandler? - - /// Closure to handle when the user stops typing - open var userStoppedTypingHandler: (() -> Void)? - - /// Set your custom set of attributes in order to highlight the string found in each item - open var highlightAttributes: [NSAttributedString.Key: AnyObject] = [.font: UIFont.boldSystemFont(ofSize: 10)] - - /// Start showing the default loading indicator, useful for searches that take some time. - open func showLoadingIndicator() { - self.rightViewMode = .always - indicator.startAnimating() - } - - /// Force the results list to adapt to RTL languages - open var forceRightToLeft = false - - /// Hide the default loading indicator - open func stopLoadingIndicator() { - self.rightViewMode = .never - indicator.stopAnimating() - } - - /// When InlineMode is true, the suggestions appear in the same line than the entered string. It's useful for email domains suggestion for example. - open var inlineMode: Bool = false { - didSet { - if inlineMode == true { - autocorrectionType = .no - spellCheckingType = .no - } - } - } - - /// Only valid when InlineMode is true. The suggestions appear after typing the provided string (or even better a character like '@') - open var startFilteringAfter: String? - - /// Min number of characters to start filtering - open var minCharactersNumberToStartFiltering: Int = 0 - - /// Force no filtering (display the entire filtered data source) - open var forceNoFiltering: Bool = false - - /// If startFilteringAfter is set, and startSuggestingImmediately is true, the list of suggestions appear immediately - open var startSuggestingImmediately = false - - /// Allow to decide the comparision options - open var comparisonOptions: NSString.CompareOptions = [.caseInsensitive] - - /// Set the results list's header - open var resultsListHeader: UIView? - - // Move the table around to customize for your layout - open var tableXOffset: CGFloat = 0.0 - open var tableYOffset: CGFloat = 0.0 - open var tableCornerRadius: CGFloat = 2.0 - open var tableBottomMargin: CGFloat = 10.0 - - //////////////////////////////////////////////////////////////////////// - // Private implementation - - fileprivate var tableView: UITableView? - fileprivate var shadowView: UIView? - fileprivate var direction: Direction = .down - fileprivate var fontConversionRate: CGFloat = 0.7 - fileprivate var keyboardFrame: CGRect? - fileprivate var timer: Timer? = nil - fileprivate var placeholderLabel: UILabel? - fileprivate static let cellIdentifier = "APSearchTextFieldCell" - fileprivate let indicator = UIActivityIndicatorView(style: .gray) - fileprivate var maxTableViewSize: CGFloat = 0 - - fileprivate var filteredResults = [SearchTextFieldItem]() - fileprivate var filterDataSource = [SearchTextFieldItem]() { - didSet { - filter(forceShowAll: forceNoFiltering) - buildSearchTableView() - - if startVisibleWithoutInteraction { - textFieldDidChange() - } - } - } - - fileprivate var currentInlineItem = "" - - deinit { - NotificationCenter.default.removeObserver(self) - } - - open override func willMove(toWindow newWindow: UIWindow?) { - super.willMove(toWindow: newWindow) - tableView?.removeFromSuperview() - } - - override open func willMove(toSuperview newSuperview: UIView?) { - super.willMove(toSuperview: newSuperview) - - self.addTarget(self, action: #selector(SearchTextField.textFieldDidChange), for: .editingChanged) - self.addTarget(self, action: #selector(SearchTextField.textFieldDidBeginEditing), for: .editingDidBegin) - self.addTarget(self, action: #selector(SearchTextField.textFieldDidEndEditing), for: .editingDidEnd) - self.addTarget(self, action: #selector(SearchTextField.textFieldDidEndEditingOnExit), for: .editingDidEndOnExit) - - NotificationCenter.default.addObserver(self, selector: #selector(SearchTextField.keyboardWillShow(_:)), name: UIResponder.keyboardWillShowNotification, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(SearchTextField.keyboardWillHide(_:)), name: UIResponder.keyboardWillHideNotification, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(SearchTextField.keyboardDidChangeFrame(_:)), name: UIResponder.keyboardDidChangeFrameNotification, object: nil) - } - - override open func layoutSubviews() { - super.layoutSubviews() - - if inlineMode { - buildPlaceholderLabel() - } else { - buildSearchTableView() - } - - // Create the loading indicator - indicator.hidesWhenStopped = true - self.rightView = indicator - } - - override open func rightViewRect(forBounds bounds: CGRect) -> CGRect { - var rightFrame = super.rightViewRect(forBounds: bounds) - rightFrame.origin.x -= 5 - return rightFrame - } - - // Create the filter table and shadow view - fileprivate func buildSearchTableView() { - guard let tableView = tableView, let shadowView = shadowView else { - self.tableView = UITableView(frame: CGRect.zero) - self.shadowView = UIView(frame: CGRect.zero) - buildSearchTableView() - return - } - - tableView.layer.masksToBounds = true - tableView.layer.borderWidth = theme.borderWidth > 0 ? theme.borderWidth : 0.5 - tableView.dataSource = self - tableView.delegate = self - tableView.separatorInset = UIEdgeInsets.zero - tableView.tableHeaderView = resultsListHeader - if forceRightToLeft { - tableView.semanticContentAttribute = .forceRightToLeft - } - - shadowView.backgroundColor = UIColor.lightText - shadowView.layer.shadowColor = UIColor.black.cgColor - shadowView.layer.shadowOffset = CGSize.zero - shadowView.layer.shadowOpacity = 1 - - self.window?.addSubview(tableView) - - redrawSearchTableView() - } - - fileprivate func buildPlaceholderLabel() { - var newRect = self.placeholderRect(forBounds: self.bounds) - var caretRect = self.caretRect(for: self.beginningOfDocument) - let textRect = self.textRect(forBounds: self.bounds) - - if let range = textRange(from: beginningOfDocument, to: endOfDocument) { - caretRect = self.firstRect(for: range) - } - - newRect.origin.x = caretRect.origin.x + caretRect.size.width + textRect.origin.x - newRect.size.width = newRect.size.width - newRect.origin.x - - if let placeholderLabel = placeholderLabel { - placeholderLabel.font = self.font - placeholderLabel.frame = newRect - } else { - placeholderLabel = UILabel(frame: newRect) - placeholderLabel?.font = self.font - placeholderLabel?.backgroundColor = UIColor.clear - placeholderLabel?.lineBreakMode = .byClipping - - if let placeholderColor = self.attributedPlaceholder?.attribute(NSAttributedString.Key.foregroundColor, at: 0, effectiveRange: nil) as? UIColor { - placeholderLabel?.textColor = placeholderColor - } else { - placeholderLabel?.textColor = UIColor ( red: 0.8, green: 0.8, blue: 0.8, alpha: 1.0 ) - } - - self.addSubview(placeholderLabel!) - } - } - - // Re-set frames and theme colors - fileprivate func redrawSearchTableView() { - if inlineMode { - tableView?.isHidden = true - return - } - - if let tableView = tableView { - guard let frame = self.superview?.convert(self.frame, to: nil) else { return } - - //TableViews use estimated cell heights to calculate content size until they - // are on-screen. We must set this to the theme cell height to avoid getting an - // incorrect contentSize when we have specified non-standard fonts and/or - // cellHeights in the theme. We do it here to ensure updates to these settings - // are recognized if changed after the tableView is created - tableView.estimatedRowHeight = theme.cellHeight - if self.direction == .down { - - var tableHeight: CGFloat = 0 - if keyboardIsShowing, let keyboardHeight = keyboardFrame?.size.height { - tableHeight = min((tableView.contentSize.height), (UIScreen.main.bounds.size.height - frame.origin.y - frame.height - keyboardHeight)) - } else { - tableHeight = min((tableView.contentSize.height), (UIScreen.main.bounds.size.height - frame.origin.y - frame.height)) - } - - if maxResultsListHeight > 0 { - tableHeight = min(tableHeight, CGFloat(maxResultsListHeight)) - } - - // Set a bottom margin of 10p - if tableHeight < tableView.contentSize.height { - tableHeight -= tableBottomMargin - } - - var tableViewFrame = CGRect(x: 0, y: 0, width: frame.size.width - 4, height: tableHeight) - tableViewFrame.origin = self.convert(tableViewFrame.origin, to: nil) - tableViewFrame.origin.x += 2 + tableXOffset - tableViewFrame.origin.y += frame.size.height + 2 + tableYOffset - self.tableView?.frame.origin = tableViewFrame.origin // Avoid animating from (0, 0) when displaying at launch - UIView.animate(withDuration: 0.2, animations: { [weak self] in - self?.tableView?.frame = tableViewFrame - }) - - var shadowFrame = CGRect(x: 0, y: 0, width: frame.size.width - 6, height: 1) - shadowFrame.origin = self.convert(shadowFrame.origin, to: nil) - shadowFrame.origin.x += 3 - shadowFrame.origin.y = tableView.frame.origin.y - shadowView!.frame = shadowFrame - } else { - let tableHeight = min((tableView.contentSize.height), (UIScreen.main.bounds.size.height - frame.origin.y - theme.cellHeight)) - UIView.animate(withDuration: 0.2, animations: { [weak self] in - self?.tableView?.frame = CGRect(x: frame.origin.x + 2, y: (frame.origin.y - tableHeight), width: frame.size.width - 4, height: tableHeight) - self?.shadowView?.frame = CGRect(x: frame.origin.x + 3, y: (frame.origin.y + 3), width: frame.size.width - 6, height: 1) - }) - } - - superview?.bringSubviewToFront(tableView) - superview?.bringSubviewToFront(shadowView!) - - if self.isFirstResponder { - superview?.bringSubviewToFront(self) - } - - tableView.layer.borderColor = theme.borderColor.cgColor - tableView.layer.cornerRadius = tableCornerRadius - tableView.separatorColor = theme.separatorColor - tableView.backgroundColor = theme.bgColor - - tableView.reloadData() - } - } - - // Handle keyboard events - @objc open func keyboardWillShow(_ notification: Notification) { - if !keyboardIsShowing && isEditing { - keyboardIsShowing = true - keyboardFrame = ((notification as NSNotification).userInfo![UIResponder.keyboardFrameEndUserInfoKey] as! NSValue).cgRectValue - interactedWith = true - prepareDrawTableResult() - } - } - - @objc open func keyboardWillHide(_ notification: Notification) { - if keyboardIsShowing { - keyboardIsShowing = false - direction = .down - redrawSearchTableView() - } - } - - @objc open func keyboardDidChangeFrame(_ notification: Notification) { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in - self?.keyboardFrame = ((notification as NSNotification).userInfo![UIResponder.keyboardFrameEndUserInfoKey] as! NSValue).cgRectValue - self?.prepareDrawTableResult() - } - } - - @objc open func typingDidStop() { - self.userStoppedTypingHandler?() - } - - // Handle text field changes - @objc open func textFieldDidChange() { - if !inlineMode && tableView == nil { - buildSearchTableView() - } - - interactedWith = true - - // Detect pauses while typing - timer?.invalidate() - timer = Timer.scheduledTimer(timeInterval: typingStoppedDelay, target: self, selector: #selector(SearchTextField.typingDidStop), userInfo: self, repeats: false) - - if text!.isEmpty { - clearResults() - tableView?.reloadData() - if startVisible || startVisibleWithoutInteraction { - filter(forceShowAll: true) - } - self.placeholderLabel?.text = "" - } else { - filter(forceShowAll: forceNoFiltering) - prepareDrawTableResult() - } - - buildPlaceholderLabel() - } - - @objc open func textFieldDidBeginEditing() { - if (startVisible || startVisibleWithoutInteraction) && text!.isEmpty { - clearResults() - filter(forceShowAll: true) - } - placeholderLabel?.attributedText = nil - } - - @objc open func textFieldDidEndEditing() { - clearResults() - tableView?.reloadData() - placeholderLabel?.attributedText = nil - } - - @objc open func textFieldDidEndEditingOnExit() { - if let firstElement = filteredResults.first { - if let itemSelectionHandler = self.itemSelectionHandler { - itemSelectionHandler(filteredResults, 0) - } - else { - if inlineMode, let filterAfter = startFilteringAfter { - let stringElements = self.text?.components(separatedBy: filterAfter) - - self.text = stringElements!.first! + filterAfter + firstElement.title - } else { - self.text = firstElement.title - } - } - } - } - - open func hideResultsList() { - if let tableFrame:CGRect = tableView?.frame { - let newFrame = CGRect(x: tableFrame.origin.x, y: tableFrame.origin.y, width: tableFrame.size.width, height: 0.0) - UIView.animate(withDuration: 0.2, animations: { [weak self] in - self?.tableView?.frame = newFrame - }) - - } - } - - fileprivate func filter(forceShowAll addAll: Bool) { - clearResults() - - if text!.count < minCharactersNumberToStartFiltering { - return - } - - for i in 0 ..< filterDataSource.count { - - let item = filterDataSource[i] - - if !inlineMode { - // Find text in title and subtitle - let titleFilterRange = (item.title as NSString).range(of: text!, options: comparisonOptions) - let subtitleFilterRange = item.subtitle != nil ? (item.subtitle! as NSString).range(of: text!, options: comparisonOptions) : NSMakeRange(NSNotFound, 0) - - if titleFilterRange.location != NSNotFound || subtitleFilterRange.location != NSNotFound || addAll { - item.attributedTitle = NSMutableAttributedString(string: item.title) - item.attributedSubtitle = NSMutableAttributedString(string: (item.subtitle != nil ? item.subtitle! : "")) - - item.attributedTitle!.setAttributes(highlightAttributes, range: titleFilterRange) - - if subtitleFilterRange.location != NSNotFound { - item.attributedSubtitle!.setAttributes(highlightAttributesForSubtitle(), range: subtitleFilterRange) - } - - filteredResults.append(item) - } - } else { - var textToFilter = text!.lowercased() - - if inlineMode, let filterAfter = startFilteringAfter { - if let suffixToFilter = textToFilter.components(separatedBy: filterAfter).last, (suffixToFilter != "" || startSuggestingImmediately == true), textToFilter != suffixToFilter { - textToFilter = suffixToFilter - } else { - placeholderLabel?.text = "" - return - } - } - - if item.title.lowercased().hasPrefix(textToFilter) { - let indexFrom = textToFilter.index(textToFilter.startIndex, offsetBy: textToFilter.count) - let itemSuffix = item.title[indexFrom...] - - item.attributedTitle = NSMutableAttributedString(string: String(itemSuffix)) - filteredResults.append(item) - } - } - } - - tableView?.reloadData() - - if inlineMode { - handleInlineFiltering() - } - } - - // Clean filtered results - fileprivate func clearResults() { - filteredResults.removeAll() - tableView?.removeFromSuperview() - } - - // Look for Font attribute, and if it exists, adapt to the subtitle font size - fileprivate func highlightAttributesForSubtitle() -> [NSAttributedString.Key: AnyObject] { - var highlightAttributesForSubtitle = [NSAttributedString.Key: AnyObject]() - - for attr in highlightAttributes { - if attr.0 == NSAttributedString.Key.font { - let fontName = (attr.1 as! UIFont).fontName - let pointSize = (attr.1 as! UIFont).pointSize * fontConversionRate - highlightAttributesForSubtitle[attr.0] = UIFont(name: fontName, size: pointSize) - } else { - highlightAttributesForSubtitle[attr.0] = attr.1 - } - } - - return highlightAttributesForSubtitle - } - - // Handle inline behaviour - func handleInlineFiltering() { - if let text = self.text { - if text == "" { - self.placeholderLabel?.attributedText = nil - } else { - if let firstResult = filteredResults.first { - self.placeholderLabel?.attributedText = firstResult.attributedTitle - } else { - self.placeholderLabel?.attributedText = nil - } - } - } - } - - // MARK: - Prepare for draw table result - - fileprivate func prepareDrawTableResult() { - guard let frame = self.superview?.convert(self.frame, to: UIApplication.shared.keyWindow) else { return } - if let keyboardFrame = keyboardFrame { - var newFrame = frame - newFrame.size.height += theme.cellHeight - - if keyboardFrame.intersects(newFrame) { - direction = .up - } else { - direction = .down - } - - redrawSearchTableView() - } else { - if self.center.y + theme.cellHeight > UIApplication.shared.keyWindow!.frame.size.height { - direction = .up - } else { - direction = .down - } - } - } -} - -extension SearchTextField: UITableViewDelegate, UITableViewDataSource { - public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - tableView.isHidden = !interactedWith || (filteredResults.count == 0) - shadowView?.isHidden = !interactedWith || (filteredResults.count == 0) - - if maxNumberOfResults > 0 { - return min(filteredResults.count, maxNumberOfResults) - } else { - return filteredResults.count - } - } - - public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - var cell = tableView.dequeueReusableCell(withIdentifier: SearchTextField.cellIdentifier) - - if cell == nil { - cell = UITableViewCell(style: .subtitle, reuseIdentifier: SearchTextField.cellIdentifier) - } - - cell!.backgroundColor = UIColor.clear - cell!.layoutMargins = UIEdgeInsets.zero - cell!.preservesSuperviewLayoutMargins = false - cell!.textLabel?.font = theme.font - cell!.detailTextLabel?.font = UIFont(name: theme.font.fontName, size: theme.font.pointSize * fontConversionRate) - cell!.textLabel?.textColor = theme.fontColor - cell!.detailTextLabel?.textColor = theme.subtitleFontColor - - cell!.textLabel?.text = filteredResults[(indexPath as NSIndexPath).row].title - cell!.detailTextLabel?.text = filteredResults[(indexPath as NSIndexPath).row].subtitle - cell!.textLabel?.attributedText = filteredResults[(indexPath as NSIndexPath).row].attributedTitle - cell!.detailTextLabel?.attributedText = filteredResults[(indexPath as NSIndexPath).row].attributedSubtitle - - cell!.imageView?.image = filteredResults[(indexPath as NSIndexPath).row].image - - cell!.selectionStyle = .none - - return cell! - } - - public func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { - return theme.cellHeight - } - - public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - if itemSelectionHandler == nil { - self.text = filteredResults[(indexPath as NSIndexPath).row].title - } else { - let index = indexPath.row - itemSelectionHandler!(filteredResults, index) - } - - clearResults() - } -} - -//////////////////////////////////////////////////////////////////////// -// Search Text Field Theme - -public struct SearchTextFieldTheme { - public var cellHeight: CGFloat - public var bgColor: UIColor - public var borderColor: UIColor - public var borderWidth : CGFloat = 0 - public var separatorColor: UIColor - public var font: UIFont - public var fontColor: UIColor - public var subtitleFontColor: UIColor - public var placeholderColor: UIColor? - - init(cellHeight: CGFloat, bgColor:UIColor, borderColor: UIColor, separatorColor: UIColor, font: UIFont, fontColor: UIColor, subtitleFontColor: UIColor? = nil) { - self.cellHeight = cellHeight - self.borderColor = borderColor - self.separatorColor = separatorColor - self.bgColor = bgColor - self.font = font - self.fontColor = fontColor - self.subtitleFontColor = subtitleFontColor ?? fontColor - } - - public static func lightTheme() -> SearchTextFieldTheme { - return SearchTextFieldTheme(cellHeight: 30, bgColor: UIColor (red: 1, green: 1, blue: 1, alpha: 0.6), borderColor: UIColor (red: 0.9, green: 0.9, blue: 0.9, alpha: 1.0), separatorColor: UIColor.clear, font: UIFont.systemFont(ofSize: 10), fontColor: UIColor.black) - } - - public static func darkTheme() -> SearchTextFieldTheme { - return SearchTextFieldTheme(cellHeight: 30, bgColor: UIColor (red: 0.8, green: 0.8, blue: 0.8, alpha: 0.6), borderColor: UIColor (red: 0.7, green: 0.7, blue: 0.7, alpha: 1.0), separatorColor: UIColor.clear, font: UIFont.systemFont(ofSize: 10), fontColor: UIColor.white) - } -} - -//////////////////////////////////////////////////////////////////////// -// Filter Item - -open class SearchTextFieldItem { - // Private vars - fileprivate var attributedTitle: NSMutableAttributedString? - fileprivate var attributedSubtitle: NSMutableAttributedString? - - // Public interface - public var title: String - public var subtitle: String? - public var image: UIImage? - - public init(title: String, subtitle: String?, image: UIImage?) { - self.title = title - self.subtitle = subtitle - self.image = image - } - - public init(title: String, subtitle: String?) { - self.title = title - self.subtitle = subtitle - } - - public init(title: String) { - self.title = title - } -} - -public typealias SearchTextFieldItemHandler = (_ filteredResults: [SearchTextFieldItem], _ index: Int) -> Void - -//////////////////////////////////////////////////////////////////////// -// Suggestions List Direction - -enum Direction { - case down - case up -} diff --git a/SearchTextField/Assets/.gitkeep b/Sources/SearchTextField/Classes/.gitkeep similarity index 100% rename from SearchTextField/Assets/.gitkeep rename to Sources/SearchTextField/Classes/.gitkeep diff --git a/Sources/SearchTextField/Classes/SearchTextField.swift b/Sources/SearchTextField/Classes/SearchTextField.swift new file mode 100755 index 0000000..458a248 --- /dev/null +++ b/Sources/SearchTextField/Classes/SearchTextField.swift @@ -0,0 +1,767 @@ +// +// SearchTextField.swift +// SearchTextField +// +// Created by Alejandro Pasccon on 4/20/16. +// Copyright © 2016 Alejandro Pasccon. All rights reserved. +// + +#if canImport(UIKit) +import UIKit + +open class SearchTextField: UITextField { + //////////////////////////////////////////////////////////////////////// + // Public interface + + /// Maximum number of results to be shown in the suggestions list + open var maxNumberOfResults = 0 + + /// Maximum height of the results list + open var maxResultsListHeight = 0 + + /// Indicate if this field has been interacted with yet + open var interactedWith = false + + /// Indicate if keyboard is showing or not + open var keyboardIsShowing = false + + /// How long to wait before deciding typing has stopped + open var typingStoppedDelay = 0.8 + + /// Set the visual theme + /// + /// Pre-defined Themes available: + /// - SearchTextFieldTheme.lightTheme() + /// - SearchTextFieldTheme.darkTheme() + open var theme = SearchTextFieldTheme.lightTheme() { + didSet { + tableView?.reloadData() + + if let placeholderColor = theme.placeholderColor { + if let placeholderString = placeholder { + self.attributedPlaceholder = NSAttributedString( + string: placeholderString, + attributes: [NSAttributedString.Key.foregroundColor: placeholderColor] + ) + } + + self.placeholderLabel?.textColor = placeholderColor + } + + if let hightlightedFont = self.highlightAttributes[.font] as? UIFont { + self.highlightAttributes[.font] = hightlightedFont.withSize(self.theme.font.pointSize) + } + } + } + + /// Show the suggestions list without filter when the text field is focused + open var startVisible = false + + /// Show the suggestions list without filter even if the text field is not focused + open var startVisibleWithoutInteraction = false { + didSet { + if startVisibleWithoutInteraction { + textFieldDidChange() + } + } + } + + /// Set an array of SearchTextFieldItem's to be used for suggestions + open func filterItems(_ items: [SearchTextFieldItem]) { + filterDataSource = items + } + + /// Set an array of strings to be used for suggestions + open func filterStrings(_ strings: [String]) { + var items = [SearchTextFieldItem]() + + for value in strings { + items.append(SearchTextFieldItem(title: value)) + } + + filterItems(items) + } + + /// Closure to handle when the user pick an item + open var itemSelectionHandler: SearchTextFieldItemHandler? + + /// Closure to handle when the user stops typing + open var userStoppedTypingHandler: (() -> Void)? + + /// Set your custom set of attributes in order to highlight the string found in each item + open var highlightAttributes: [NSAttributedString.Key: AnyObject] = [.font: UIFont.boldSystemFont(ofSize: 10)] + + /// Start showing the default loading indicator, useful for searches that take some time. + open func showLoadingIndicator() { + rightViewMode = .always + indicator.startAnimating() + } + + /// Force the results list to adapt to RTL languages + open var forceRightToLeft = false + + /// Hide the default loading indicator + open func stopLoadingIndicator() { + rightViewMode = .never + indicator.stopAnimating() + } + + /// When InlineMode is true, the suggestions appear in the same line than the entered string. + /// It's useful for email domains suggestion for example. + open var inlineMode: Bool = false { + didSet { + if inlineMode == true { + autocorrectionType = .no + spellCheckingType = .no + } + } + } + + /// Only valid when InlineMode is true. + /// The suggestions appear after typing the provided string (or even better a character like '@') + open var startFilteringAfter: String? + + /// Min number of characters to start filtering + open var minCharactersNumberToStartFiltering: Int = 0 + + /// Force no filtering (display the entire filtered data source) + open var forceNoFiltering: Bool = false + + /// If startFilteringAfter is set, + /// and startSuggestingImmediately is true, the list of suggestions appear immediately + open var startSuggestingImmediately = false + + /// Allow to decide the comparision options + open var comparisonOptions: NSString.CompareOptions = [.caseInsensitive] + + /// Set the results list's header + open var resultsListHeader: UIView? + + // Move the table around to customize for your layout + open var tableXOffset: CGFloat = 0.0 + open var tableYOffset: CGFloat = 0.0 + open var tableCornerRadius: CGFloat = 2.0 + open var tableBottomMargin: CGFloat = 10.0 + + //////////////////////////////////////////////////////////////////////// + // Private implementation + + fileprivate var tableView: UITableView? + fileprivate var shadowView: UIView? + fileprivate var direction: Direction = .down + fileprivate var fontConversionRate: CGFloat = 0.7 + fileprivate var keyboardFrame: CGRect? + fileprivate var timer: Timer? + fileprivate var placeholderLabel: UILabel? + fileprivate static let cellIdentifier = "APSearchTextFieldCell" + fileprivate let indicator = UIActivityIndicatorView(style: .gray) + fileprivate var maxTableViewSize: CGFloat = 0 + + fileprivate var filteredResults = [SearchTextFieldItem]() + fileprivate var filterDataSource = [SearchTextFieldItem]() { + didSet { + filter(forceShowAll: forceNoFiltering) + buildSearchTableView() + + if startVisibleWithoutInteraction { + textFieldDidChange() + } + } + } + + fileprivate var currentInlineItem = "" + + deinit { + NotificationCenter.default.removeObserver(self) + } + + override open func willMove(toWindow newWindow: UIWindow?) { + super.willMove(toWindow: newWindow) + tableView?.removeFromSuperview() + } + + override open func willMove(toSuperview newSuperview: UIView?) { + super.willMove(toSuperview: newSuperview) + + addTarget(self, action: #selector(SearchTextField.textFieldDidChange), for: .editingChanged) + addTarget(self, action: #selector(SearchTextField.textFieldDidBeginEditing), for: .editingDidBegin) + addTarget(self, action: #selector(SearchTextField.textFieldDidEndEditing), for: .editingDidEnd) + addTarget(self, action: #selector(SearchTextField.textFieldDidEndEditingOnExit), for: .editingDidEndOnExit) + + NotificationCenter.default.addObserver( + self, + selector: #selector(SearchTextField.keyboardWillShow(_:)), + name: UIResponder.keyboardWillShowNotification, object: nil + ) + + NotificationCenter.default.addObserver( + self, + selector: #selector(SearchTextField.keyboardWillHide(_:)), + name: UIResponder.keyboardWillHideNotification, object: nil + ) + + NotificationCenter.default.addObserver( + self, + selector: #selector(SearchTextField.keyboardDidChangeFrame(_:)), + name: UIResponder.keyboardDidChangeFrameNotification, object: nil + ) + } + + override open func layoutSubviews() { + super.layoutSubviews() + + if inlineMode { + buildPlaceholderLabel() + } else { + buildSearchTableView() + } + + // Create the loading indicator + indicator.hidesWhenStopped = true + rightView = indicator + } + + override open func rightViewRect(forBounds bounds: CGRect) -> CGRect { + var rightFrame = super.rightViewRect(forBounds: bounds) + rightFrame.origin.x -= 5 + return rightFrame + } + + // Create the filter table and shadow view + fileprivate func buildSearchTableView() { + guard let tableView = tableView, let shadowView = shadowView else { + self.tableView = UITableView(frame: CGRect.zero) + self.shadowView = UIView(frame: CGRect.zero) + buildSearchTableView() + return + } + + tableView.layer.masksToBounds = true + tableView.layer.borderWidth = theme.borderWidth > 0 ? theme.borderWidth : 0.5 + tableView.dataSource = self + tableView.delegate = self + tableView.separatorInset = UIEdgeInsets.zero + tableView.tableHeaderView = resultsListHeader + if forceRightToLeft { + tableView.semanticContentAttribute = .forceRightToLeft + } + + shadowView.backgroundColor = UIColor.lightText + shadowView.layer.shadowColor = UIColor.black.cgColor + shadowView.layer.shadowOffset = CGSize.zero + shadowView.layer.shadowOpacity = 1 + + window?.addSubview(tableView) + + redrawSearchTableView() + } + + fileprivate func buildPlaceholderLabel() { + var newRect = placeholderRect(forBounds: bounds) + var caretRect = self.caretRect(for: beginningOfDocument) + let textRect = self.textRect(forBounds: bounds) + + if let range = textRange(from: beginningOfDocument, to: endOfDocument) { + caretRect = firstRect(for: range) + } + + newRect.origin.x = caretRect.origin.x + caretRect.size.width + textRect.origin.x + newRect.size.width -= newRect.origin.x + + if let placeholderLabel = placeholderLabel { + placeholderLabel.font = font + placeholderLabel.frame = newRect + } else { + placeholderLabel = UILabel(frame: newRect) + placeholderLabel?.font = font + placeholderLabel?.backgroundColor = UIColor.clear + placeholderLabel?.lineBreakMode = .byClipping + + if let placeholderColor = attributedPlaceholder? + .attribute(NSAttributedString.Key.foregroundColor, at: 0, effectiveRange: nil) as? UIColor { + placeholderLabel?.textColor = placeholderColor + } else { + placeholderLabel?.textColor = UIColor(red: 0.8, green: 0.8, blue: 0.8, alpha: 1.0) + } + + addSubview(placeholderLabel!) + } + } + + // Re-set frames and theme colors + fileprivate func redrawSearchTableView() { + if inlineMode { + tableView?.isHidden = true + return + } + + if let tableView = tableView { + guard let frame = superview?.convert(self.frame, to: nil) else { return } + + // TableViews use estimated cell heights to calculate content size until they + // are on-screen. We must set this to the theme cell height to avoid getting an + // incorrect contentSize when we have specified non-standard fonts and/or + // cellHeights in the theme. We do it here to ensure updates to these settings + // are recognized if changed after the tableView is created + tableView.estimatedRowHeight = theme.cellHeight + if direction == .down { + var tableHeight: CGFloat = 0 + if keyboardIsShowing, let keyboardHeight = keyboardFrame?.size.height { + tableHeight = min( + tableView.contentSize.height, + UIScreen.main.bounds.size.height - frame.origin.y - frame.height - keyboardHeight + ) + } else { + tableHeight = min( + tableView.contentSize.height, + UIScreen.main.bounds.size.height - frame.origin.y - frame.height + ) + } + + if maxResultsListHeight > 0 { + tableHeight = min(tableHeight, CGFloat(maxResultsListHeight)) + } + + // Set a bottom margin of 10p + if tableHeight < tableView.contentSize.height { + tableHeight -= tableBottomMargin + } + + var tableViewFrame = CGRect(x: 0, y: 0, width: frame.size.width - 4, height: tableHeight) + tableViewFrame.origin = convert(tableViewFrame.origin, to: nil) + tableViewFrame.origin.x += 2 + tableXOffset + tableViewFrame.origin.y += frame.size.height + 2 + tableYOffset + + // Avoid animating from (0, 0) when displaying at launch + self.tableView?.frame.origin = tableViewFrame.origin + + UIView.animate(withDuration: 0.2, animations: { [weak self] in + self?.tableView?.frame = tableViewFrame + }) + + var shadowFrame = CGRect(x: 0, y: 0, width: frame.size.width - 6, height: 1) + shadowFrame.origin = convert(shadowFrame.origin, to: nil) + shadowFrame.origin.x += 3 + shadowFrame.origin.y = tableView.frame.origin.y + shadowView!.frame = shadowFrame + } else { + let tableHeight = min( + tableView.contentSize.height, + UIScreen.main.bounds.size.height - frame.origin.y - theme.cellHeight + ) + + UIView.animate(withDuration: 0.2, animations: { [weak self] in + self?.tableView?.frame = CGRect( + x: frame.origin.x + 2, + y: frame.origin.y - tableHeight, width: frame.size.width - 4, height: tableHeight + ) + + self?.shadowView?.frame = CGRect( + x: frame.origin.x + 3, y: frame.origin.y + 3, + width: frame.size.width - 6, height: 1 + ) + }) + } + + superview?.bringSubviewToFront(tableView) + superview?.bringSubviewToFront(shadowView!) + + if isFirstResponder { + superview?.bringSubviewToFront(self) + } + + tableView.layer.borderColor = theme.borderColor.cgColor + tableView.layer.cornerRadius = tableCornerRadius + tableView.separatorColor = theme.separatorColor + tableView.backgroundColor = theme.bgColor + + tableView.reloadData() + } + } + + // Handle keyboard events + @objc open func keyboardWillShow(_ notification: Notification) { + guard let userInfo = (notification as NSNotification).userInfo, + let keyboardFrameInfo = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue else { return } + + if !keyboardIsShowing, isEditing { + keyboardIsShowing = true + keyboardFrame = keyboardFrameInfo.cgRectValue + interactedWith = true + prepareDrawTableResult() + } + } + + @objc open func keyboardWillHide(_: Notification) { + if keyboardIsShowing { + keyboardIsShowing = false + direction = .down + redrawSearchTableView() + } + } + + @objc open func keyboardDidChangeFrame(_ notification: Notification) { + guard let userInfo = (notification as NSNotification).userInfo, + let keyboardFrameInfo = userInfo[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue else { return } + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { [weak self] in + self?.keyboardFrame = keyboardFrameInfo.cgRectValue + self?.prepareDrawTableResult() + } + } + + @objc open func typingDidStop() { + userStoppedTypingHandler?() + } + + // Handle text field changes + @objc open func textFieldDidChange() { + if !inlineMode, tableView == nil { + buildSearchTableView() + } + + interactedWith = true + + // Detect pauses while typing + timer?.invalidate() + timer = Timer.scheduledTimer( + timeInterval: typingStoppedDelay, + target: self, + selector: #selector(SearchTextField.typingDidStop), + userInfo: self, + repeats: false + ) + + if text!.isEmpty { + clearResults() + tableView?.reloadData() + if startVisible || startVisibleWithoutInteraction { + filter(forceShowAll: true) + } + placeholderLabel?.text = "" + } else { + filter(forceShowAll: forceNoFiltering) + prepareDrawTableResult() + } + + buildPlaceholderLabel() + } + + @objc open func textFieldDidBeginEditing() { + if startVisible || startVisibleWithoutInteraction, text!.isEmpty { + clearResults() + filter(forceShowAll: true) + } + placeholderLabel?.attributedText = nil + } + + @objc open func textFieldDidEndEditing() { + clearResults() + tableView?.reloadData() + placeholderLabel?.attributedText = nil + } + + @objc open func textFieldDidEndEditingOnExit() { + if let firstElement = filteredResults.first { + if let itemSelectionHandler = self.itemSelectionHandler { + itemSelectionHandler(filteredResults, 0) + } else { + if inlineMode, let filterAfter = startFilteringAfter { + let stringElements = text?.components(separatedBy: filterAfter) + + text = stringElements!.first! + filterAfter + firstElement.title + } else { + text = firstElement.title + } + } + } + } + + open func hideResultsList() { + if let tableFrame: CGRect = tableView?.frame { + let newFrame = CGRect( + x: tableFrame.origin.x, + y: tableFrame.origin.y, + width: tableFrame.size.width, + height: 0.0 + ) + + UIView.animate(withDuration: 0.2, animations: { [weak self] in + self?.tableView?.frame = newFrame + }) + } + } + + fileprivate func filter(forceShowAll addAll: Bool) { + clearResults() + + if text!.count < minCharactersNumberToStartFiltering { + return + } + + for idx in 0 ..< filterDataSource.count { + let item = filterDataSource[idx] + + if !inlineMode { + // Find text in title and subtitle + let titleFilterRange = (item.title as NSString).range(of: text!, options: comparisonOptions) + let subtitleFilterRange = item.subtitle != nil + ? (item.subtitle! as NSString).range(of: text!, options: comparisonOptions) + : NSRange(location: NSNotFound, length: 0) + + if titleFilterRange.location != NSNotFound || subtitleFilterRange.location != NSNotFound || addAll { + item.attributedTitle = NSMutableAttributedString(string: item.title) + item.attributedSubtitle = NSMutableAttributedString(string: item.subtitle != nil ? item.subtitle! : "") + + item.attributedTitle!.setAttributes(highlightAttributes, range: titleFilterRange) + + if subtitleFilterRange.location != NSNotFound { + item.attributedSubtitle!.setAttributes(highlightAttributesForSubtitle(), range: subtitleFilterRange) + } + + filteredResults.append(item) + } + } else { + var textToFilter = text!.lowercased() + + if inlineMode, let filterAfter = startFilteringAfter { + if let suffixToFilter = textToFilter.components(separatedBy: filterAfter).last, + suffixToFilter != "" || startSuggestingImmediately == true, + textToFilter != suffixToFilter { + textToFilter = suffixToFilter + } else { + placeholderLabel?.text = "" + return + } + } + + if item.title.lowercased().hasPrefix(textToFilter) { + let indexFrom = textToFilter.index(textToFilter.startIndex, offsetBy: textToFilter.count) + let itemSuffix = item.title[indexFrom...] + + item.attributedTitle = NSMutableAttributedString(string: String(itemSuffix)) + filteredResults.append(item) + } + } + } + + tableView?.reloadData() + + if inlineMode { + handleInlineFiltering() + } + } + + // Clean filtered results + fileprivate func clearResults() { + filteredResults.removeAll() + tableView?.removeFromSuperview() + } + + // Look for Font attribute, and if it exists, adapt to the subtitle font size + fileprivate func highlightAttributesForSubtitle() -> [NSAttributedString.Key: AnyObject] { + var highlightAttributesForSubtitle = [NSAttributedString.Key: AnyObject]() + + for attr in highlightAttributes { + if attr.0 == NSAttributedString.Key.font, let font = attr.1 as? UIFont { + let fontName = font.fontName + let pointSize = font.pointSize * fontConversionRate + highlightAttributesForSubtitle[attr.0] = UIFont(name: fontName, size: pointSize) + } else { + highlightAttributesForSubtitle[attr.0] = attr.1 + } + } + + return highlightAttributesForSubtitle + } + + // Handle inline behaviour + func handleInlineFiltering() { + if let text = self.text { + if text == "" { + placeholderLabel?.attributedText = nil + } else { + if let firstResult = filteredResults.first { + placeholderLabel?.attributedText = firstResult.attributedTitle + } else { + placeholderLabel?.attributedText = nil + } + } + } + } + + // MARK: - Prepare for draw table result + + fileprivate func prepareDrawTableResult() { + guard let frame = superview?.convert(self.frame, to: UIApplication.shared.keyWindow) else { return } + if let keyboardFrame = keyboardFrame { + var newFrame = frame + newFrame.size.height += theme.cellHeight + + if keyboardFrame.intersects(newFrame) { + direction = .up + } else { + direction = .down + } + + redrawSearchTableView() + } else { + if center.y + theme.cellHeight > UIApplication.shared.keyWindow!.frame.size.height { + direction = .up + } else { + direction = .down + } + } + } +} + +extension SearchTextField: UITableViewDelegate, UITableViewDataSource { + public func tableView(_ tableView: UITableView, numberOfRowsInSection _: Int) -> Int { + tableView.isHidden = !interactedWith || (filteredResults.count == 0) + shadowView?.isHidden = !interactedWith || (filteredResults.count == 0) + + if maxNumberOfResults > 0 { + return min(filteredResults.count, maxNumberOfResults) + } else { + return filteredResults.count + } + } + + public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + guard indexPath.row < filteredResults.count else { + return UITableViewCell(style: .subtitle, reuseIdentifier: SearchTextField.cellIdentifier) + } + + let cell = tableView.dequeueReusableCell(withIdentifier: SearchTextField.cellIdentifier) + ?? UITableViewCell(style: .subtitle, reuseIdentifier: SearchTextField.cellIdentifier) + + cell.backgroundColor = UIColor.clear + cell.layoutMargins = UIEdgeInsets.zero + cell.preservesSuperviewLayoutMargins = false + cell.textLabel?.font = theme.font + cell.detailTextLabel?.font = UIFont(name: theme.font.fontName, size: theme.font.pointSize * fontConversionRate) + cell.textLabel?.textColor = theme.fontColor + cell.detailTextLabel?.textColor = theme.subtitleFontColor + + cell.textLabel?.text = filteredResults[indexPath.row].title + cell.detailTextLabel?.text = filteredResults[indexPath.row].subtitle + cell.textLabel?.attributedText = filteredResults[indexPath.row].attributedTitle + cell.detailTextLabel?.attributedText = filteredResults[indexPath.row].attributedSubtitle + + cell.imageView?.image = filteredResults[indexPath.row].image + + cell.selectionStyle = .none + + return cell + } + + public func tableView(_: UITableView, heightForRowAt _: IndexPath) -> CGFloat { + theme.cellHeight + } + + public func tableView(_: UITableView, didSelectRowAt indexPath: IndexPath) { + if itemSelectionHandler == nil { + text = filteredResults[(indexPath as NSIndexPath).row].title + } else { + let index = indexPath.row + itemSelectionHandler!(filteredResults, index) + } + + clearResults() + } +} + +//////////////////////////////////////////////////////////////////////// +// Search Text Field Theme + +public struct SearchTextFieldTheme { + public var cellHeight: CGFloat + public var bgColor: UIColor + public var borderColor: UIColor + public var borderWidth: CGFloat = 0 + public var separatorColor: UIColor + public var font: UIFont + public var fontColor: UIColor + public var subtitleFontColor: UIColor + public var placeholderColor: UIColor? + + init( + cellHeight: CGFloat, + bgColor: UIColor, + borderColor: UIColor, + separatorColor: UIColor, + font: UIFont, + fontColor: UIColor, + subtitleFontColor: UIColor? = nil + ) { + self.cellHeight = cellHeight + self.borderColor = borderColor + self.separatorColor = separatorColor + self.bgColor = bgColor + self.font = font + self.fontColor = fontColor + self.subtitleFontColor = subtitleFontColor ?? fontColor + } + + public static func lightTheme() -> SearchTextFieldTheme { + SearchTextFieldTheme( + cellHeight: 30, + bgColor: UIColor(red: 1, green: 1, blue: 1, alpha: 0.6), + borderColor: UIColor(red: 0.9, green: 0.9, blue: 0.9, alpha: 1.0), + separatorColor: UIColor.clear, + font: UIFont.systemFont(ofSize: 10), + fontColor: UIColor.black + ) + } + + public static func darkTheme() -> SearchTextFieldTheme { + SearchTextFieldTheme( + cellHeight: 30, + bgColor: UIColor(red: 0.8, green: 0.8, blue: 0.8, alpha: 0.6), + borderColor: UIColor(red: 0.7, green: 0.7, blue: 0.7, alpha: 1.0), + separatorColor: UIColor.clear, + font: UIFont.systemFont(ofSize: 10), + fontColor: UIColor.white + ) + } +} + +//////////////////////////////////////////////////////////////////////// +// Filter Item + +open class SearchTextFieldItem { + // Private vars + fileprivate var attributedTitle: NSMutableAttributedString? + fileprivate var attributedSubtitle: NSMutableAttributedString? + + // Public interface + public var title: String + public var subtitle: String? + public var image: UIImage? + + public init(title: String, subtitle: String?, image: UIImage?) { + self.title = title + self.subtitle = subtitle + self.image = image + } + + public init(title: String, subtitle: String?) { + self.title = title + self.subtitle = subtitle + } + + public init(title: String) { + self.title = title + } +} + +public typealias SearchTextFieldItemHandler = (_ filteredResults: [SearchTextFieldItem], _ index: Int) -> Void + +//////////////////////////////////////////////////////////////////////// +// Suggestions List Direction + +enum Direction { + case down + case up +} +#endif diff --git a/Tests/SearchTextFieldTests/SearchTextFieldTest.swift b/Tests/SearchTextFieldTests/SearchTextFieldTest.swift new file mode 100644 index 0000000..b82a369 --- /dev/null +++ b/Tests/SearchTextFieldTests/SearchTextFieldTest.swift @@ -0,0 +1,25 @@ +// +// SearchTextFieldTest.swift +// +// +// Created by Brent Mifsud on 2020-05-15. +// + +#if canImport(UIKit) + @testable import SearchTextField + import XCTest + + class SearchTextFieldTest: XCTestCase { + override func setUp() { + // add setup code + } + + override func tearDown() { + // add tear down code + } + + func testSearchTextField() { + // add test code + } + } +#endif