latest version

This commit is contained in:
Okan Yücel 2022-09-26 16:12:32 +03:00
parent d9d2d25141
commit 3ad434f219
104 changed files with 214166 additions and 1 deletions

0
.github/.keep vendored Normal file
View File

83
.gitignore vendored Normal file
View File

@ -0,0 +1,83 @@
# Xcode
#
# gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore
## Build generated
build/
DerivedData/
FeedbackReporterIOS/DrivedData
*.ipa
*.app.dSYM.zip
## Various settings
*.pbxuser
!default.pbxuser
*.mode1v3
!default.mode1v3
*.mode2v3
!default.mode2v3
*.perspectivev3
!default.perspectivev3
xcuserdata/
## Other
*.moved-aside
*.xccheckout
*.xcscmblueprint
## Obj-C/Swift specific
*.hmap
*.ipa
*.dSYM.zip
*.dSYM
## Playgrounds
timeline.xctimeline
playground.xcworkspace
# Swift Package Manager
#
# Add this line if you want to avoid checking in source code from Swift Package Manager dependencies.
# Packages/
# Package.pins
# Package.resolved
# *.xcodeproj
#
# Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata
# hence it is not needed unless you have added a package configuration file to your project
# .swiftpm
.build/
# CocoaPods
#
# We recommend against adding the Pods directory to your .gitignore. However
# you should judge for yourself, the pros and cons are mentioned at:
# https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
#
Pods/
# Carthage
#
# Add this line if you want to avoid checking in source code from Carthage dependencies.
# Carthage/Checkouts
Carthage/Build
vendor
# fastlane
#
# It is recommended to not store the screenshots in the git repo.
# Instead, use fastlane to re-generate the screenshots whenever they are needed.
# For more information about the recommended setup visit:
# https://docs.fastlane.tools/best-practices/source-control/#source-control
fastlane/report.xml
fastlane/Preview.html
fastlane/screenshots
fastlane/test_output
.DS_Store
fastlane/README.md
reports/
firebase-debug.log

17
.swiftlint.yml Normal file
View File

@ -0,0 +1,17 @@
disabled_rules:
- trailing_whitespace
excluded:
- .build
- Build
- fastlane
- vendor
- Gemfile.locked
- Gemfile
identifier_name:
min_length: # only min_length
error: 3 # only error
max_length:
warning: 50
excluded: # excluded via string array
- id

7
API/.gitignore vendored Normal file
View File

@ -0,0 +1,7 @@
.DS_Store
/.build
/Packages
/*.xcodeproj
xcuserdata/
DerivedData/
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata

View File

@ -0,0 +1,77 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1320"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "API"
BuildableName = "API"
BlueprintName = "API"
ReferencedContainer = "container:">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "APITests"
BuildableName = "APITests"
BlueprintName = "APITests"
ReferencedContainer = "container:">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "API"
BuildableName = "API"
BlueprintName = "API"
ReferencedContainer = "container:">
</BuildableReference>
</MacroExpansion>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

28
API/Package.swift Normal file
View File

@ -0,0 +1,28 @@
// swift-tools-version:5.5
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "API",
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.
.library(
name: "API",
targets: ["API"])
],
dependencies: [
// Dependencies declare other packages that this package depends on.
// .package(url: /* package url */, from: "1.0.0"),
],
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 this package depends on.
.target(
name: "API",
dependencies: []),
.testTarget(
name: "APITests",
dependencies: ["API"])
]
)

3
API/README.md Normal file
View File

@ -0,0 +1,3 @@
# API
A description of this package.

View File

@ -0,0 +1,32 @@
//
// City.swift
//
//
// Created by okan.yucel on 5.03.2022.
//
import Foundation
// MARK: - City
public struct City: Codable {
public struct Request: Encodable {
public init() {
}
}
public let country, name: String?
public let id: Int?
public let coordinate: Coordinate?
enum CodingKeys: String, CodingKey {
case country, name
case id = "_id"
case coordinate = "coord"
}
}
// MARK: - Coord
public struct Coordinate: Codable {
public let lon, lat: Double?
}

View File

@ -0,0 +1,30 @@
//
// Stubber.swift
//
//
// Created by okan.yucel on 5.03.2022.
//
import Foundation
public enum Stubber {
public static func getDataFromLocal<T: Decodable>(
fileName: String, completion: @escaping (Result<T, Error>) -> Void
) {
DispatchQueue.global(qos: .userInitiated).async {
if let path = Bundle(for: EmptyClass.self).path(forResource: fileName, ofType: "json") {
do {
let data = try Data(contentsOf: URL(fileURLWithPath: path), options: .mappedIfSafe)
let response = try JSONDecoder().decode(T.self, from: data)
completion(.success(response))
} catch {
completion(.failure(error))
}
} else {
completion(.failure(NSError(domain: "no json file", code: 400, userInfo: nil)))
}
}
}
}
private final class EmptyClass {}

View File

@ -0,0 +1,11 @@
import XCTest
@testable import API
final class APITests: XCTestCase {
func testExample() throws {
// This is an example of a functional test case.
// Use XCTAssert and related functions to verify your tests produce the correct
// results.
XCTAssertEqual(API().text, "Hello, World!")
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@ -0,0 +1,79 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1320"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "780ABAE527D37B1700AC241A"
BuildableName = "CitieSearch.app"
BlueprintName = "CitieSearch"
ReferencedContainer = "container:CitieSearch.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Development"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Development"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
enableGPUValidationMode = "1"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "780ABAE527D37B1700AC241A"
BuildableName = "CitieSearch.app"
BlueprintName = "CitieSearch"
ReferencedContainer = "container:CitieSearch.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Production"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "780ABAE527D37B1700AC241A"
BuildableName = "CitieSearch.app"
BlueprintName = "CitieSearch"
ReferencedContainer = "container:CitieSearch.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Development">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Production"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@ -0,0 +1,24 @@
//
// AppDelegate.swift
// CitieSearch
//
// Created by okan.yucel on 5.03.2022.
//
import UIKit
@UIApplicationMain class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
private lazy var configuration: AppConfiguration = .init()
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
// Override point for customization after application launch.
configuration.configure()
return true
}
}

View File

@ -0,0 +1,11 @@
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,98 @@
{
"images" : [
{
"idiom" : "iphone",
"scale" : "2x",
"size" : "20x20"
},
{
"idiom" : "iphone",
"scale" : "3x",
"size" : "20x20"
},
{
"idiom" : "iphone",
"scale" : "2x",
"size" : "29x29"
},
{
"idiom" : "iphone",
"scale" : "3x",
"size" : "29x29"
},
{
"idiom" : "iphone",
"scale" : "2x",
"size" : "40x40"
},
{
"idiom" : "iphone",
"scale" : "3x",
"size" : "40x40"
},
{
"idiom" : "iphone",
"scale" : "2x",
"size" : "60x60"
},
{
"idiom" : "iphone",
"scale" : "3x",
"size" : "60x60"
},
{
"idiom" : "ipad",
"scale" : "1x",
"size" : "20x20"
},
{
"idiom" : "ipad",
"scale" : "2x",
"size" : "20x20"
},
{
"idiom" : "ipad",
"scale" : "1x",
"size" : "29x29"
},
{
"idiom" : "ipad",
"scale" : "2x",
"size" : "29x29"
},
{
"idiom" : "ipad",
"scale" : "1x",
"size" : "40x40"
},
{
"idiom" : "ipad",
"scale" : "2x",
"size" : "40x40"
},
{
"idiom" : "ipad",
"scale" : "1x",
"size" : "76x76"
},
{
"idiom" : "ipad",
"scale" : "2x",
"size" : "76x76"
},
{
"idiom" : "ipad",
"scale" : "2x",
"size" : "83.5x83.5"
},
{
"idiom" : "ios-marketing",
"scale" : "1x",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "ic16BackIos.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "ic16BackIos@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "ic16BackIos@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 353 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 485 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 621 B

View File

@ -0,0 +1,23 @@
{
"images" : [
{
"filename" : "ic16Close.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"filename" : "ic16Close@2x.png",
"idiom" : "universal",
"scale" : "2x"
},
{
"filename" : "ic16Close@3x.png",
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 395 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 579 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 764 B

View File

@ -0,0 +1,58 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="19529" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="19519"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" axis="vertical" translatesAutoresizingMaskIntoConstraints="NO" id="EKs-Pd-bW6">
<rect key="frame" x="0.0" y="44" width="414" height="818"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="eLE-lI-eD2">
<rect key="frame" x="32" y="32" width="350" height="368.5"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</view>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="CitieSearch" textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="djx-ey-9hK">
<rect key="frame" x="32" y="400.5" width="350" height="17"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleHeadline"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="FR6-ro-W9E">
<rect key="frame" x="32" y="417.5" width="350" height="368.5"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</view>
</subviews>
<constraints>
<constraint firstItem="FR6-ro-W9E" firstAttribute="height" secondItem="eLE-lI-eD2" secondAttribute="height" id="cwf-Ml-c4a"/>
</constraints>
<edgeInsets key="layoutMargins" top="32" left="32" bottom="32" right="32"/>
</stackView>
</subviews>
<viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstItem="EKs-Pd-bW6" firstAttribute="bottom" secondItem="6Tk-OE-BBY" secondAttribute="bottom" id="Cj3-5x-KPj"/>
<constraint firstItem="EKs-Pd-bW6" firstAttribute="leading" secondItem="6Tk-OE-BBY" secondAttribute="leading" id="cmz-Ut-Dmd"/>
<constraint firstItem="EKs-Pd-bW6" firstAttribute="trailing" secondItem="6Tk-OE-BBY" secondAttribute="trailing" id="ksf-nh-Lfk"/>
<constraint firstItem="EKs-Pd-bW6" firstAttribute="top" secondItem="6Tk-OE-BBY" secondAttribute="top" id="tCI-A4-bGN"/>
</constraints>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
</document>

View File

@ -0,0 +1,53 @@
//
// AppConfiguration.swift
// CitieSearch
//
// Created by okan.yucel on 5.03.2022.
//
import Foundation
protocol AppConfigurationLogic {
func configure()
}
struct AppConfiguration {
public init(configurable: AppConfigurationLogic = AppConfiguration.configuration) {
self.configurable = configurable
}
private var configurable: AppConfigurationLogic
func configure() {
configurable.configure()
}
static var type: EnvironmentType {
#if DEBUG
return .development
#elseif UAT
return .staging
#elseif PROD
return .production
#endif
}
enum EnvironmentType {
case development
case staging
case production
}
}
extension AppConfiguration {
static var configuration: AppConfigurationLogic {
switch type {
case .development:
return DEVConfiguration()
case .staging:
return UATConfiguration()
case .production:
return PRODConfiguration()
}
}
}

View File

@ -0,0 +1,14 @@
//
// DEVConfiguration.swift
// CitieSearch
//
// Created by okan.yucel on 5.03.2022.
//
import Foundation
struct DEVConfiguration: AppConfigurationLogic {
func configure() {
Logger.info("dev_launched".localizeIt)
}
}

View File

@ -0,0 +1,14 @@
//
// PRODConfiguration.swift
// CitieSearch
//
// Created by okan.yucel on 5.03.2022.
//
import Foundation
struct PRODConfiguration: AppConfigurationLogic {
func configure() {
// TODO: extra configuration if need for prod
}
}

View File

@ -0,0 +1,14 @@
//
// UATConfiguration.swift
// CitieSearch
//
// Created by okan.yucel on 5.03.2022.
//
import Foundation
struct UATConfiguration: AppConfigurationLogic {
func configure() {
// TODO: extra configuration if need for uat
}
}

View File

@ -0,0 +1,14 @@
//
// Development.xcconfig
// CitieSearch
//
// Created by okan.yucel on 5.03.2022.
//
// Configuration settings file format documentation can be found at:
// https://help.apple.com/xcode/#/dev745c5c974
PRODUCT_DISPLAY_NAME = Cities Search DEV
PRODUCT_BUNDLE_NAME = CitieSearch
PRODUCT_BUNDLE_IDENTIFIER = com.kona.CitieSearch.dev
INITIAL_STORYBOARD_NAME = Splash

View File

@ -0,0 +1,14 @@
//
// Production.xcconfig
// CitieSearch
//
// Created by okan.yucel on 5.03.2022.
//
// Configuration settings file format documentation can be found at:
// https://help.apple.com/xcode/#/dev745c5c974
PRODUCT_DISPLAY_NAME = Cities Search
PRODUCT_BUNDLE_NAME = CitieSearch
PRODUCT_BUNDLE_IDENTIFIER = com.kona.CitieSearch
INITIAL_STORYBOARD_NAME = Splash

View File

@ -0,0 +1,14 @@
//
// Staging.xcconfig
// CitieSearch
//
// Created by okan.yucel on 5.03.2022.
//
// Configuration settings file format documentation can be found at:
// https://help.apple.com/xcode/#/dev745c5c974
PRODUCT_DISPLAY_NAME = Cities Search UAT
PRODUCT_BUNDLE_NAME = CitieSearch
PRODUCT_BUNDLE_IDENTIFIER = com.kona.CitieSearch.uat
INITIAL_STORYBOARD_NAME = Splash

View File

@ -0,0 +1,14 @@
//
// String+Extensions.swift
// CitieSearch
//
// Created by okan.yucel on 7.03.2022.
//
import Foundation
extension String {
var localizeIt: String {
return NSLocalizedString(self, comment: "")
}
}

View File

@ -0,0 +1,16 @@
//
// Logger.swift
// CitieSearch
//
// Created by okan.yucel on 5.03.2022.
//
import Foundation
import Extensions
enum Logger {
static func info(_ message: String) {
guard AppConfiguration.type == .development else { return }
print("[\(Date().fullFormatDate)] \(message)")
}
}

16
CitieSearch/Info.plist Normal file
View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>CFBundleName</key>
<string>$(PRODUCT_BUNDLE_NAME)</string>
<key>UIMainStoryboardFile</key>
<string>$(INITIAL_STORYBOARD_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleDisplayName</key>
<string>$(PRODUCT_DISPLAY_NAME)</string>
</dict>
</plist>

View File

@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="19529" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="nxe-uw-h5I">
<device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="19519"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--City Map View Controller-->
<scene sceneID="D3m-6j-bBI">
<objects>
<viewController id="nxe-uw-h5I" customClass="CityMapViewController" customModule="CitieSearch" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="chM-je-gA4">
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<mapView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" mapType="standard" translatesAutoresizingMaskIntoConstraints="NO" id="ooM-HG-1Mf">
<rect key="frame" x="0.0" y="44" width="414" height="818"/>
</mapView>
</subviews>
<viewLayoutGuide key="safeArea" id="acR-sl-ate"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<constraints>
<constraint firstItem="ooM-HG-1Mf" firstAttribute="top" secondItem="acR-sl-ate" secondAttribute="top" id="4Xs-2k-dEL"/>
<constraint firstItem="acR-sl-ate" firstAttribute="bottom" secondItem="ooM-HG-1Mf" secondAttribute="bottom" id="8tV-SE-vxX"/>
<constraint firstItem="acR-sl-ate" firstAttribute="trailing" secondItem="ooM-HG-1Mf" secondAttribute="trailing" id="GSi-VP-nZH"/>
<constraint firstItem="ooM-HG-1Mf" firstAttribute="leading" secondItem="acR-sl-ate" secondAttribute="leading" id="hTZ-9d-P1t"/>
</constraints>
</view>
<connections>
<outlet property="mapView" destination="ooM-HG-1Mf" id="VwP-bU-mtY"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="cZo-gs-e4f" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="105.79710144927537" y="27.455357142857142"/>
</scene>
</scenes>
<resources>
<systemColor name="systemBackgroundColor">
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</systemColor>
</resources>
</document>

View File

@ -0,0 +1,36 @@
//
// CityMapInteractor.swift
// CitieSearch
//
// Created by okan.yucel on 6.03.2022.
//
import Foundation
protocol CityMapBusinessLogic: AnyObject {
func fetchCity(request: CityMap.FetchCity.Request)
}
protocol CityMapDataStore: AnyObject {
var city: CityItem? { get set }
}
final class CityMapInteractor: CityMapBusinessLogic, CityMapDataStore {
var presenter: CityMapPresentationLogic?
var worker: CityMapWorkingLogic?
var city: CityItem?
init(worker: CityMapWorkingLogic) {
self.worker = worker
}
func fetchCity(request: CityMap.FetchCity.Request) {
guard let city = city else {
presenter?.presentAlert(message: "an_error_occured".localizeIt)
return
}
presenter?.presentCity(response: .init(city: city))
}
}

View File

@ -0,0 +1,33 @@
//
// CityMapModels.swift
// CitieSearch
//
// Created by okan.yucel on 6.03.2022.
//
import Foundation
import MapKit
// swiftlint:disable nesting
enum CityMap {
enum FetchCity {
struct Request {
}
struct Response {
let city: CityItem
}
struct ViewModel {
let annotation: MKPointAnnotation
let region: MKCoordinateRegion
let title: String
}
}
}
// swiftlint:enable nesting

View File

@ -0,0 +1,41 @@
//
// CityMapPresenter.swift
// CitieSearch
//
// Created by okan.yucel on 6.03.2022.
//
import Foundation
import MapKit
protocol CityMapPresentationLogic: AnyObject {
func presentCity(response: CityMap.FetchCity.Response)
func presentAlert(message: String)
}
final class CityMapPresenter: CityMapPresentationLogic {
weak var viewController: CityMapDisplayLogic?
func presentCity(response: CityMap.FetchCity.Response) {
let location = CLLocationCoordinate2D(
latitude: response.city.latitude, longitude: response.city.longitude
)
let span = MKCoordinateSpan(latitudeDelta: 0.2, longitudeDelta: 0.2)
let region = MKCoordinateRegion(center: location, span: span)
let annotation = MKPointAnnotation()
annotation.coordinate = location
annotation.title = response.city.title
viewController?.displayCity(
viewModel: .init(annotation: annotation, region: region, title: response.city.title)
)
}
func presentAlert(message: String) {
viewController?.displayAlert(message: message)
}
}

View File

@ -0,0 +1,23 @@
//
// CityMapRouter.swift
// CitieSearch
//
// Created by okan.yucel on 6.03.2022.
//
import Foundation
protocol CityMapRoutingLogic: AnyObject {
}
protocol CityMapDataPassing: AnyObject {
var dataStore: CityMapDataStore? { get }
}
final class CityMapRouter: CityMapRoutingLogic, CityMapDataPassing {
weak var viewController: CityMapViewController?
var dataStore: CityMapDataStore?
}

View File

@ -0,0 +1,77 @@
//
// CityMapViewController.swift
// CitieSearch
//
// Created by okan.yucel on 6.03.2022.
//
import MapKit
import Extensions
protocol CityMapDisplayLogic: AnyObject {
func displayCity(viewModel: CityMap.FetchCity.ViewModel)
func displayAlert(message: String)
}
final class CityMapViewController: UIViewController, BarButtonConfigurable, AlertPresentableLogic {
var interactor: CityMapBusinessLogic?
var router: (CityMapRoutingLogic & CityMapDataPassing)?
@IBOutlet private weak var mapView: MKMapView?
// MARK: Object lifecycle
override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
// MARK: Setup
private func setup() {
let viewController = self
let interactor = CityMapInteractor(worker: CityMapWorker())
let presenter = CityMapPresenter()
let router = CityMapRouter()
viewController.interactor = interactor
viewController.router = router
interactor.presenter = presenter
presenter.viewController = viewController
router.viewController = viewController
router.dataStore = interactor
}
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
interactor?.fetchCity(request: .init())
}
private func setupUI() {
addBarButtonItem(ofType: .dismiss(.right))
}
}
extension CityMapViewController: CityMapDisplayLogic {
func displayCity(viewModel: CityMap.FetchCity.ViewModel) {
title = viewModel.title
mapView?.setRegion(viewModel.region, animated: true)
mapView?.addAnnotation(viewModel.annotation)
}
func displayAlert(message: String) {
presentAlert(
"error".localizeIt, message: message, actionTitle: "ok".localizeIt
) { [weak self] in
self?.dismiss(animated: true, completion: nil)
}
}
}

View File

@ -0,0 +1,16 @@
//
// CityMapWorker.swift
// CitieSearch
//
// Created by okan.yucel on 6.03.2022.
//
import Foundation
protocol CityMapWorkingLogic: AnyObject {
}
final class CityMapWorker: CityMapWorkingLogic {
}

View File

@ -0,0 +1,26 @@
//
// CityCell.swift
// CitieSearch
//
// Created by okan.yucel on 6.03.2022.
//
import UIKit
protocol CityItem {
var title: String { get }
var subtitle: String { get }
var latitude: Double { get }
var longitude: Double { get }
}
class CityCell: UITableViewCell {
@IBOutlet private weak var titleLabel: UILabel?
@IBOutlet private weak var subtitleLabel: UILabel?
func configureCell(_ item: CityItem) {
titleLabel?.text = item.title
subtitleLabel?.text = item.subtitle
}
}

View File

@ -0,0 +1,66 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="19529" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="19519"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
<tableViewCell contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" selectionStyle="none" indentationWidth="10" rowHeight="141" id="KGk-i7-Jjw" customClass="CityCell" customModule="CitieSearch" customModuleProvider="target">
<rect key="frame" x="0.0" y="0.0" width="320" height="141"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" tableViewCell="KGk-i7-Jjw" id="H2p-sc-9uM">
<rect key="frame" x="0.0" y="0.0" width="320" height="141"/>
<autoresizingMask key="autoresizingMask"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="REM-rH-vGN">
<rect key="frame" x="0.0" y="0.0" width="320" height="141"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="kY8-Dp-Alg">
<rect key="frame" x="16" y="16" width="288" height="17"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleHeadline"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Tc2-wL-w8E">
<rect key="frame" x="16" y="41" width="288" height="14.5"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleSubhead"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="1ZS-e7-df3" userLabel="Seperator">
<rect key="frame" x="16" y="63.5" width="288" height="1"/>
<color key="backgroundColor" red="0.77647058823529413" green="0.77647058823529413" blue="0.78431372549019607" alpha="0.5" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstAttribute="height" constant="1" id="jr2-m0-j4x"/>
</constraints>
</view>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="yOI-TE-0jV" userLabel="Spacer">
<rect key="frame" x="16" y="72.5" width="288" height="68.5"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</view>
</subviews>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<edgeInsets key="layoutMargins" top="16" left="16" bottom="0.0" right="16"/>
</stackView>
</subviews>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstItem="REM-rH-vGN" firstAttribute="leading" secondItem="H2p-sc-9uM" secondAttribute="leading" id="6oo-uX-aYA"/>
<constraint firstAttribute="bottom" secondItem="REM-rH-vGN" secondAttribute="bottom" id="AnX-cC-IYX"/>
<constraint firstItem="REM-rH-vGN" firstAttribute="top" secondItem="H2p-sc-9uM" secondAttribute="top" id="sTs-jW-3SE"/>
<constraint firstAttribute="trailing" secondItem="REM-rH-vGN" secondAttribute="trailing" id="sXi-xd-AbI"/>
</constraints>
</tableViewCellContentView>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<connections>
<outlet property="subtitleLabel" destination="Tc2-wL-w8E" id="LR1-fd-7YT"/>
<outlet property="titleLabel" destination="kY8-Dp-Alg" id="18I-Ez-Q64"/>
</connections>
<point key="canvasLocation" x="137.68115942028987" y="114.84375"/>
</tableViewCell>
</objects>
</document>

View File

@ -0,0 +1,52 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="19529" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="nxe-uw-h5I">
<device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="19519"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--City Search View Controller-->
<scene sceneID="D3m-6j-bBI">
<objects>
<viewController id="nxe-uw-h5I" customClass="CitySearchViewController" customModule="CitieSearch" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="chM-je-gA4">
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<tableView clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="plain" separatorStyle="none" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="-1" estimatedSectionHeaderHeight="-1" sectionFooterHeight="-1" estimatedSectionFooterHeight="-1" translatesAutoresizingMaskIntoConstraints="NO" id="pwK-s8-xCL">
<rect key="frame" x="0.0" y="44" width="414" height="818"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<connections>
<outlet property="dataSource" destination="nxe-uw-h5I" id="IqF-TJ-gKK"/>
<outlet property="delegate" destination="nxe-uw-h5I" id="KqN-IJ-suR"/>
</connections>
</tableView>
</subviews>
<viewLayoutGuide key="safeArea" id="acR-sl-ate"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
<constraints>
<constraint firstItem="acR-sl-ate" firstAttribute="bottom" secondItem="pwK-s8-xCL" secondAttribute="bottom" id="OMn-eb-lHM"/>
<constraint firstItem="pwK-s8-xCL" firstAttribute="leading" secondItem="acR-sl-ate" secondAttribute="leading" id="UVX-yJ-Wbi"/>
<constraint firstItem="pwK-s8-xCL" firstAttribute="top" secondItem="acR-sl-ate" secondAttribute="top" id="fh0-hz-u27"/>
<constraint firstItem="acR-sl-ate" firstAttribute="trailing" secondItem="pwK-s8-xCL" secondAttribute="trailing" id="sMk-VH-N6m"/>
</constraints>
</view>
<connections>
<outlet property="tableView" destination="pwK-s8-xCL" id="LLv-uw-Xhe"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="cZo-gs-e4f" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="107" y="28"/>
</scene>
</scenes>
<resources>
<systemColor name="systemBackgroundColor">
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</systemColor>
</resources>
</document>

View File

@ -0,0 +1,70 @@
//
// CitySearchInteractor.swift
// CitieSearch
//
// Created by okan.yucel on 5.03.2022.
//
import Foundation
import Swiftrie
import API
protocol CitySearchBusinessLogic: AnyObject {
func fetchCities(request: CitySearch.Search.Request)
}
protocol CitySearchDataStore: AnyObject {
var trie: Swiftrie { get set }
var cities: [City] { get set }
}
final class CitySearchInteractor: CitySearchBusinessLogic, CitySearchDataStore {
var presenter: CitySearchPresentationLogic?
var worker: CitySearchWorkingLogic?
var trie: Swiftrie = Swiftrie(swiftriables: [])
var cities: [City] = []
init(worker: CitySearchWorkingLogic) {
self.worker = worker
}
func fetchCities(request: CitySearch.Search.Request) {
guard !request.text.isEmpty else {
trie.cancelSearch()
presenter?.presentCities(response: .init(cities: cities))
return
}
let throttle = CitySearch.SearchThrottle.calculate(
count: request.text.count
).second.rounded(toPlaces: 2)
let throttleMessage = "throttle_for_searching".localizeIt.replacingOccurrences(
of: "{throttle}", with: "\(throttle)"
)
Logger.info(throttleMessage)
let searchStartDate = Date()
trie.findItems(
prefix: request.text,
throttle: throttle,
type: City.self,
completion: { [weak self] results in
let seconds = Date().timeIntervalSince(searchStartDate).rounded(toPlaces: 4)
let searchMessage = "search_response_message".localizeIt.replacingOccurrences(
of: "{city_count}", with: "\(results.count)"
).replacingOccurrences(
of: "{seconds}", with: "\(seconds)"
).replacingOccurrences(of: "{text}", with: request.text)
Logger.info(searchMessage)
self?.presenter?.presentCities(response: .init(cities: results))
}
)
}
}

View File

@ -0,0 +1,67 @@
//
// CitySearchModels.swift
// CitieSearch
//
// Created by okan.yucel on 5.03.2022.
//
import Foundation
import API
import Swiftrie
extension City: CityItem {
var title: String {
return "\(name ?? ""), \(country ?? "")"
}
var subtitle: String {
let latitudeMessage = "latitude".localizeIt + ": \(coordinate?.lat ?? 0)"
let longitudeMessage = "longitude".localizeIt + ": \(coordinate?.lon ?? 0)"
return latitudeMessage + "\n" + longitudeMessage
}
var latitude: Double {
return coordinate?.lat ?? 0
}
var longitude: Double {
return coordinate?.lon ?? 0
}
}
// swiftlint:disable nesting
enum CitySearch {
enum Search {
struct Request {
var text: String
}
struct Response {
var cities: [City]
}
struct ViewModel {
var cities: [CityItem]
}
}
enum SearchThrottle {
case calculate(count: Int?)
var second: Double {
switch self {
case .calculate(let count):
guard let count = count, count != 0 else {
return 0
}
return 0
// return 0.25 / (Double(count)*2.5)
// throttle has disabled
}
}
}
}
// swiftlint:enable nesting

View File

@ -0,0 +1,21 @@
//
// CitySearchPresenter.swift
// CitieSearch
//
// Created by okan.yucel on 5.03.2022.
//
import Foundation
protocol CitySearchPresentationLogic: AnyObject {
func presentCities(response: CitySearch.Search.Response)
}
final class CitySearchPresenter: CitySearchPresentationLogic {
weak var viewController: CitySearchDisplayLogic?
func presentCities(response: CitySearch.Search.Response) {
viewController?.displayCities(viewModel: .init(cities: response.cities))
}
}

View File

@ -0,0 +1,31 @@
//
// CitySearchRouter.swift
// CitieSearch
//
// Created by okan.yucel on 5.03.2022.
//
import Foundation
import UIKit
protocol CitySearchRoutingLogic: AnyObject {
func routeToMap(item: CityItem)
}
protocol CitySearchDataPassing: AnyObject {
var dataStore: CitySearchDataStore? { get }
}
final class CitySearchRouter: CitySearchRoutingLogic, CitySearchDataPassing {
weak var viewController: CitySearchViewController?
var dataStore: CitySearchDataStore?
func routeToMap(item: CityItem) {
let mapViewController: CityMapViewController = UIApplication.getViewController()
mapViewController.router?.dataStore?.city = item
let navigationController = UINavigationController(rootViewController: mapViewController)
viewController?.present(navigationController, animated: true, completion: nil)
}
}

View File

@ -0,0 +1,121 @@
//
// CitySearchViewController.swift
// CitieSearch
//
// Created by okan.yucel on 5.03.2022.
//
import UIKit
import Extensions
protocol CitySearchDisplayLogic: AnyObject {
func displayCities(viewModel: CitySearch.Search.ViewModel)
}
final class CitySearchViewController: UIViewController {
var interactor: CitySearchBusinessLogic?
var router: (CitySearchRoutingLogic & CitySearchDataPassing)?
@IBOutlet private weak var tableView: UITableView?
private let searchController = UISearchController(searchResultsController: nil)
private var viewModel: CitySearch.Search.ViewModel = .init(cities: [])
// MARK: Object lifecycle
override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
// MARK: Setup
private func setup() {
let viewController = self
let interactor = CitySearchInteractor(worker: CitySearchWorker())
let presenter = CitySearchPresenter()
let router = CitySearchRouter()
viewController.interactor = interactor
viewController.router = router
interactor.presenter = presenter
presenter.viewController = viewController
router.viewController = viewController
router.dataStore = interactor
}
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
interactor?.fetchCities(request: .init(text: ""))
}
private func setupUI() {
navigationItem.hidesBackButton = true
definesPresentationContext = true
navigationController?.setNavigationBarHidden(false, animated: true)
navigationController?.navigationBar.prefersLargeTitles = false
navigationItem.hidesSearchBarWhenScrolling = false
searchController.searchResultsUpdater = self
searchController.searchBar.placeholder = "type_here".localizeIt
searchController.obscuresBackgroundDuringPresentation = false
searchController.hidesNavigationBarDuringPresentation = false
navigationItem.searchController = searchController
tableView?.registerNib(CityCell.self, bundle: .main)
}
private func updateTitle() {
let cityCount = viewModel.cities.count
DispatchQueue.main.async { [weak self] in
self?.title = "search_cities_in".localizeIt.replacingOccurrences(
of: "{city_count}", with: "\(cityCount)"
)
}
}
}
extension CitySearchViewController: CitySearchDisplayLogic {
func displayCities(viewModel: CitySearch.Search.ViewModel) {
self.viewModel = viewModel
tableView?.reload(animation: false)
updateTitle()
}
}
extension CitySearchViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let item = viewModel.cities[indexPath.row]
router?.routeToMap(item: item)
}
}
extension CitySearchViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return viewModel.cities.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
guard viewModel.cities.count > indexPath.row else { return UITableViewCell() }
let cell = tableView.dequeueCell(type: CityCell.self, indexPath: indexPath)
let item = viewModel.cities[indexPath.row]
cell.configureCell(item)
return cell
}
}
extension CitySearchViewController: UISearchResultsUpdating {
func updateSearchResults(for searchController: UISearchController) {
interactor?.fetchCities(
request: .init(text: searchController.searchBar.text ?? "")
)
}
}

View File

@ -0,0 +1,16 @@
//
// CitySearchWorker.swift
// CitieSearch
//
// Created by okan.yucel on 5.03.2022.
//
import Foundation
protocol CitySearchWorkingLogic: AnyObject {
}
final class CitySearchWorker: CitySearchWorkingLogic {
}

View File

@ -0,0 +1,84 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="19529" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="d3h-md-cl4">
<device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="19519"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--Navigation Controller-->
<scene sceneID="5xY-5u-p0D">
<objects>
<navigationController id="d3h-md-cl4" sceneMemberID="viewController">
<navigationBar key="navigationBar" contentMode="scaleToFill" id="1EL-cw-rE0">
<rect key="frame" x="0.0" y="44" width="414" height="44"/>
<autoresizingMask key="autoresizingMask"/>
</navigationBar>
<connections>
<segue destination="nxe-uw-h5I" kind="relationship" relationship="rootViewController" id="kx4-DB-Yiy"/>
</connections>
</navigationController>
<placeholder placeholderIdentifier="IBFirstResponder" id="Bwq-8Y-0O3" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-1489" y="11"/>
</scene>
<!--Splash View Controller-->
<scene sceneID="D3m-6j-bBI">
<objects>
<viewController id="nxe-uw-h5I" customClass="SplashViewController" customModule="CitieSearch" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="chM-je-gA4">
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<stackView opaque="NO" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" axis="vertical" spacing="16" translatesAutoresizingMaskIntoConstraints="NO" id="Gtw-aB-985">
<rect key="frame" x="0.0" y="44" width="414" height="818"/>
<subviews>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Axh-BQ-Lag">
<rect key="frame" x="32" y="32" width="350" height="334.5"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</view>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="CitieSearch" textAlignment="center" lineBreakMode="tailTruncation" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="3yN-Xp-VYd">
<rect key="frame" x="32" y="382.5" width="350" height="17"/>
<fontDescription key="fontDescription" style="UICTFontTextStyleHeadline"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<activityIndicatorView opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" hidesWhenStopped="YES" animating="YES" style="white" translatesAutoresizingMaskIntoConstraints="NO" id="CCX-SP-nva">
<rect key="frame" x="32" y="415.5" width="350" height="20"/>
<color key="color" white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</activityIndicatorView>
<view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Q9m-7T-Cpd">
<rect key="frame" x="32" y="451.5" width="350" height="334.5"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</view>
</subviews>
<constraints>
<constraint firstItem="Q9m-7T-Cpd" firstAttribute="height" secondItem="Axh-BQ-Lag" secondAttribute="height" id="zL9-ME-aOA"/>
</constraints>
<edgeInsets key="layoutMargins" top="32" left="32" bottom="32" right="32"/>
</stackView>
</subviews>
<viewLayoutGuide key="safeArea" id="acR-sl-ate"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstItem="Gtw-aB-985" firstAttribute="top" secondItem="acR-sl-ate" secondAttribute="top" id="7V6-Zm-e0N"/>
<constraint firstItem="Gtw-aB-985" firstAttribute="leading" secondItem="acR-sl-ate" secondAttribute="leading" id="MK0-s6-dGp"/>
<constraint firstItem="acR-sl-ate" firstAttribute="bottom" secondItem="Gtw-aB-985" secondAttribute="bottom" id="uco-4h-hxH"/>
<constraint firstItem="acR-sl-ate" firstAttribute="trailing" secondItem="Gtw-aB-985" secondAttribute="trailing" id="xU4-A3-D4m"/>
</constraints>
</view>
<navigationItem key="navigationItem" id="glt-z7-hCP"/>
<nil key="simulatedTopBarMetrics"/>
<connections>
<outlet property="activityIndicator" destination="CCX-SP-nva" id="aOG-0M-vzN"/>
<outlet property="informationLabel" destination="3yN-Xp-VYd" id="pCT-sW-etH"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="cZo-gs-e4f" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-341" y="11"/>
</scene>
</scenes>
</document>

View File

@ -0,0 +1,39 @@
//
// SplashInteractor.swift
// CitieSearch
//
// Created by okan.yucel on 5.03.2022.
//
import Foundation
protocol SplashBusinessLogic: AnyObject {
func fetchCities(request: Splash.FetchCities.Request)
}
protocol SplashDataStore: AnyObject {
}
final class SplashInteractor: SplashBusinessLogic, SplashDataStore {
var presenter: SplashPresentationLogic?
var worker: SplashWorkingLogic?
init(worker: SplashWorkingLogic) {
self.worker = worker
}
func fetchCities(request: Splash.FetchCities.Request) {
presenter?.presentInformation(message: "cities_exporting".localizeIt)
worker?.getCities(request: .init(), completion: { [weak presenter] result in
switch result {
case .success(let response):
presenter?.presentInformation(message: "cities_exporting_completed".localizeIt)
presenter?.presentCities(response: .init(cities: response))
case .failure(let error):
presenter?.presentAlert(message: error.localizedDescription)
}
})
}
}

View File

@ -0,0 +1,41 @@
//
// SplashModels.swift
// CitieSearch
//
// Created by okan.yucel on 5.03.2022.
//
import Foundation
import API
import Swiftrie
import Extensions
extension City: Swiftriable {
public var prefixableText: String {
guard let name = name else { return "" }
return "\(name), \(country ?? "")"
}
}
// swiftlint:disable nesting
enum Splash {
enum FetchCities {
struct Request {
}
struct Response {
let cities: [City]
}
struct ViewModel {
let trie: Swiftrie
let cities: [City]
}
}
}
// swiftlint:enable nesting

View File

@ -0,0 +1,53 @@
//
// SplashPresenter.swift
// CitieSearch
//
// Created by okan.yucel on 5.03.2022.
//
import Foundation
import Swiftrie
import Extensions
import API
protocol SplashPresentationLogic: AnyObject {
func presentCities(response: Splash.FetchCities.Response)
func presentAlert(message: String)
func presentInformation(message: String)
}
final class SplashPresenter: SplashPresentationLogic {
weak var viewController: SplashDisplayLogic?
func presentCities(response: Splash.FetchCities.Response) {
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
self?.presentInformation(
message: "data_structure_preparing".localizeIt
)
Logger.info("ordering_cities_started".localizeIt)
let orderedCities = response.cities.sorted(
by: {$0.name?.lowercased() ?? "" < $1.name?.lowercased() ?? ""}
)
Logger.info("ordering_cities_completed".localizeIt)
Logger.info("data_structure_creating_started".localizeIt)
let trie = Swiftrie(swiftriables: orderedCities)
Logger.info("data_structure_creating_completed".localizeIt)
self?.presentInformation(message: "data_structure_ready".localizeIt)
self?.viewController?.displayCities(viewModel: .init(trie: trie, cities: orderedCities))
}
}
func presentAlert(message: String) {
viewController?.displayAlert(message: message)
}
func presentInformation(message: String) {
viewController?.displayInformation(message: "\n\(message)")
}
}

View File

@ -0,0 +1,31 @@
//
// SplashRouter.swift
// CitieSearch
//
// Created by okan.yucel on 5.03.2022.
//
import Foundation
import UIKit
protocol SplashRoutingLogic: AnyObject {
func routeToCitySearch(viewModel: Splash.FetchCities.ViewModel)
}
protocol SplashDataPassing: AnyObject {
var dataStore: SplashDataStore? { get }
}
final class SplashRouter: SplashRoutingLogic, SplashDataPassing {
weak var viewController: SplashViewController?
var dataStore: SplashDataStore?
func routeToCitySearch(viewModel: Splash.FetchCities.ViewModel) {
let searchViewController: CitySearchViewController = UIApplication.getViewController()
searchViewController.router?.dataStore?.trie = viewModel.trie
searchViewController.router?.dataStore?.cities = viewModel.cities
viewController?.navigationController?.pushViewController(searchViewController, animated: false)
}
}

View File

@ -0,0 +1,91 @@
//
// SplashViewController.swift
// CitieSearch
//
// Created by okan.yucel on 5.03.2022.
//
import UIKit
import Extensions
protocol SplashDisplayLogic: AnyObject {
func displayCities(viewModel: Splash.FetchCities.ViewModel)
func displayInformation(message: String)
func displayAlert(message: String)
}
final class SplashViewController: UIViewController, AlertPresentableLogic {
var interactor: SplashBusinessLogic?
var router: (SplashRoutingLogic & SplashDataPassing)?
@IBOutlet private weak var informationLabel: UILabel?
@IBOutlet private weak var activityIndicator: UIActivityIndicatorView?
// MARK: Object lifecycle
override init(nibName nibNameOrNil: String?, bundle nibBundleOrNil: Bundle?) {
super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil)
setup()
}
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
// MARK: Setup
private func setup() {
let viewController = self
let interactor = SplashInteractor(worker: SplashWorker())
let presenter = SplashPresenter()
let router = SplashRouter()
viewController.interactor = interactor
viewController.router = router
interactor.presenter = presenter
presenter.viewController = viewController
router.viewController = viewController
router.dataStore = interactor
}
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
fetchCities()
}
private func setupUI() {
navigationController?.setNavigationBarHidden(true, animated: false)
}
@objc private func fetchCities() {
activityIndicator?.startAnimating()
interactor?.fetchCities(request: .init())
}
}
extension SplashViewController: SplashDisplayLogic {
func displayCities(viewModel: Splash.FetchCities.ViewModel) {
DispatchQueue.main.async { [weak self] in
self?.activityIndicator?.stopAnimating()
self?.router?.routeToCitySearch(viewModel: viewModel)
}
}
func displayInformation(message: String) {
DispatchQueue.main.async { [weak informationLabel] in
informationLabel?.text?.append(contentsOf: "\n\(message)")
}
}
func displayAlert(message: String) {
presentAlert(
"error".localizeIt, message: message, actionTitle: "try_again".localizeIt
) { [weak self] in
self?.fetchCities()
}
}
}

View File

@ -0,0 +1,21 @@
//
// SplashWorker.swift
// CitieSearch
//
// Created by okan.yucel on 5.03.2022.
//
import Foundation
import API
protocol SplashWorkingLogic: AnyObject {
func getCities(request: City.Request, completion: @escaping (Result<[City], Error>) -> Void)
}
final class SplashWorker: SplashWorkingLogic {
func getCities(request: City.Request, completion: @escaping (Result<[City], Error>) -> Void) {
Stubber.getDataFromLocal(fileName: "cities", completion: completion)
}
}

View File

@ -0,0 +1,19 @@
"data_structure_preparing" = "The data structure has preparing for you.\nThanks for you patient.";
"data_structure_creating_started" = "Trie Data Structure creating has started";
"data_structure_creating_completed" = "Trie Data Structure creating has completed";
"ordering_cities_started" = "City ordering has started";
"ordering_cities_completed" = "City ordering has completed";
"data_structure_ready" = "Data structure is ready.";
"cities_exporting" = "Cities have exporting from file.";
"cities_exporting_completed" = "Exporting cities have completed.";
"search_cities_in" = "Search cities in {city_count} cities";
"type_here" = "Type here";
"throttle_for_searching" = "Throttle for searching: {throttle}";
"search_response_message" = "{city_count} cities have found in {seconds} seconds for '{text}'";
"latitude" = "Latitude";
"longitude" = "Longitude";
"dev_launched" = "Development configuration has launched.";
"error" = "Error";
"try_again" = "Try again";
"ok" = "OK";
"an_error_occured" = "An error occured, please try again later.";

View File

@ -0,0 +1,15 @@
"data_structure_preparing" = "The data structure has preparing for you.\nThanks for you patient.";
"data_structure_creating_started" = "Trie Data Structure creating has started";
"data_structure_creating_completed" = "Trie Data Structure creating has completed";
"ordering_cities_started" = "City ordering has started";
"ordering_cities_completed" = "City ordering has completed";
"data_structure_ready" = "Data structure is ready.";
"cities_exporting" = "Cities has exporting from file.";
"cities_exporting_completed" = "Exporting cities has completed.";
"search_cities_in" = "Search cities in {city_count} cities";
"type_here" = "Type here";
"throttle_for_searching" = "Throttle for searching: {throttle}";
"search_response_message" = "{city_count} has found in {seconds} seconds for '{text}'";
"latitude" = "Latitude";
"longitude" = "Longitude";
"dev_launched" = "Development configuration has launched.";

View File

@ -0,0 +1,37 @@
//
// CitieSearchTests.swift
// CitieSearchTests
//
// Created by okan.yucel on 5.03.2022.
//
import XCTest
@testable import CitieSearch
class CitieSearchTests: XCTestCase {
override func setUpWithError() throws {
// Put setup code here. This method is called before the invocation of each test method in the class.
}
override func tearDownWithError() throws {
// Put teardown code here. This method is called after the invocation of each test method in the class.
}
func testExample() throws {
// This is an example of a functional test case.
// Use XCTAssert and related functions to verify your tests produce the correct results.
// Any test you write for XCTest can be annotated as throws and async.
// Mark your test throws to produce an unexpected failure when your test encounters an uncaught error.
// Mark your test async to allow awaiting for asynchronous code to complete.
// Check the results with assertions afterwards.
}
func testPerformanceExample() throws {
// This is an example of a performance test case.
self.measure {
// Put the code you want to measure the time of here.
}
}
}

View File

@ -0,0 +1,43 @@
//
// CitieSearchUITests.swift
// CitieSearchUITests
//
// Created by okan.yucel on 5.03.2022.
//
import XCTest
class CitieSearchUITests: XCTestCase {
override func setUpWithError() throws {
// Put setup code here. This method is called before the invocation of each test method in the class.
// In UI tests it is usually best to stop immediately when a failure occurs.
continueAfterFailure = false
// In UI tests its important to set the initial state - such as interface orientation -
// required for your tests before they run. The setUp method is a good place to do this.
}
override func tearDownWithError() throws {
// Put teardown code here. This method is called after the invocation of each test method in the class.
}
func testExample() throws {
// UI tests must launch the application that they test.
let app = XCUIApplication()
app.launch()
// Use recording to get started writing UI tests.
// Use XCTAssert and related functions to verify your tests produce the correct results.
}
func testLaunchPerformance() throws {
if #available(macOS 10.15, iOS 13.0, tvOS 13.0, watchOS 7.0, *) {
// This measures how long it takes to launch your application.
measure(metrics: [XCTApplicationLaunchMetric()]) {
XCUIApplication().launch()
}
}
}
}

View File

@ -0,0 +1,32 @@
//
// CitieSearchUITestsLaunchTests.swift
// CitieSearchUITests
//
// Created by okan.yucel on 5.03.2022.
//
import XCTest
class CitieSearchUITestsLaunchTests: XCTestCase {
override class var runsForEachTargetApplicationUIConfiguration: Bool {
true
}
override func setUpWithError() throws {
continueAfterFailure = false
}
func testLaunch() throws {
let app = XCUIApplication()
app.launch()
// Insert steps here to perform after app launch but before taking a screenshot,
// such as logging into a test account or navigating somewhere in the app
let attachment = XCTAttachment(screenshot: app.screenshot())
attachment.name = "Launch Screen"
attachment.lifetime = .keepAlways
add(attachment)
}
}

7
Extensions/.gitignore vendored Normal file
View File

@ -0,0 +1,7 @@
.DS_Store
/.build
/Packages
/*.xcodeproj
xcuserdata/
DerivedData/
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata

View File

@ -0,0 +1,77 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1320"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "Extensions"
BuildableName = "Extensions"
BlueprintName = "Extensions"
ReferencedContainer = "container:">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "ExtensionsTests"
BuildableName = "ExtensionsTests"
BlueprintName = "ExtensionsTests"
ReferencedContainer = "container:">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "Extensions"
BuildableName = "Extensions"
BlueprintName = "Extensions"
ReferencedContainer = "container:">
</BuildableReference>
</MacroExpansion>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

28
Extensions/Package.swift Normal file
View File

@ -0,0 +1,28 @@
// swift-tools-version:5.5
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "Extensions",
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.
.library(
name: "Extensions",
targets: ["Extensions"])
],
dependencies: [
// Dependencies declare other packages that this package depends on.
// .package(url: /* package url */, from: "1.0.0"),
],
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 this package depends on.
.target(
name: "Extensions",
dependencies: []),
.testTarget(
name: "ExtensionsTests",
dependencies: ["Extensions"])
]
)

3
Extensions/README.md Normal file
View File

@ -0,0 +1,3 @@
# Extensions
A description of this package.

View File

@ -0,0 +1,23 @@
//
// Date+Extensions.swift
//
//
// Created by okan.yucel on 5.03.2022.
//
import Foundation
public extension Date {
/// Takes a date and converts it into String in given format
func stringFromCustomDate(format: String) -> String {
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = format
return dateFormatter.string(from: self)
}
var fullFormatDate: String {
return stringFromCustomDate(format: "yyyy-MM-dd'T'HH:mm:ss.SSS")
}
}

View File

@ -0,0 +1,60 @@
//
// DispatchQueue+Extensions.swift
//
//
// Created by okan.yucel on 6.03.2022.
//
import Foundation
public extension DispatchQueue {
/**
- parameters:
- target: Object used as the sentinel for de-duplication.
- delay: The time window for de-duplication to occur
- work: The work item to be invoked on the queue.
Performs work only once for the given target, given the time window. The last added work closure
is the work that will finally execute.
Note: This is currently only safe to call from the main thread.
Example usage:
```
DispatchQueue.main.asyncDeduped(target: self, after: 1.0) { [weak self] in
self?.doTheWork()
}
```
*/
func asyncDeduped(
target: AnyObject,
after delay: TimeInterval,
execute work: @escaping @convention(block) () -> Void
) {
let dedupeIdentifier = DispatchQueue.dedupeIdentifierFor(target)
if let existingWorkItem = DispatchQueue.workItems.removeValue(forKey: dedupeIdentifier) {
existingWorkItem.cancel()
}
let workItem = DispatchWorkItem {
DispatchQueue.workItems.removeValue(forKey: dedupeIdentifier)
for ptr in DispatchQueue.weakTargets.allObjects {
if dedupeIdentifier == DispatchQueue.dedupeIdentifierFor(ptr as AnyObject) {
work()
break
}
}
}
DispatchQueue.workItems[dedupeIdentifier] = workItem
DispatchQueue.weakTargets.addPointer(Unmanaged.passUnretained(target).toOpaque())
asyncAfter(deadline: .now() + delay, execute: workItem)
}
}
// MARK: - Static Properties for De-Duping
private extension DispatchQueue {
static var workItems = [AnyHashable: DispatchWorkItem]()
static var weakTargets = NSPointerArray.weakObjects()
static func dedupeIdentifierFor(_ object: AnyObject) -> String {
"\(Unmanaged.passUnretained(object).toOpaque())." + String(describing: object)
}
}

View File

@ -0,0 +1,16 @@
//
// Double+Extensions.swift
//
//
// Created by okan.yucel on 6.03.2022.
//
import Foundation
public extension Double {
/// Rounds the double to decimal places value
func rounded(toPlaces places: Int) -> Double {
let divisor = pow(10.0, Double(places))
return (self * divisor).rounded() / divisor
}
}

View File

@ -0,0 +1,14 @@
//
// String+Extensions.swift
//
//
// Created by okan.yucel on 5.03.2022.
//
import Foundation
public extension String {
func toObject<T: Decodable>(type: T.Type) -> T? {
return try? JSONDecoder().decode(type, from: Data(self.utf8))
}
}

View File

@ -0,0 +1,42 @@
//
// UIApplication+Extensions.swift
//
//
// Created by okan.yucel on 5.03.2022.
//
import Foundation
import UIKit
public extension UIApplication {
class func getViewController<T: UIViewController>(
inScene named: String? = nil,
rootViewController: Bool = true
) -> T {
let controllerName = String(describing: T.self)
let storyboardName = named ?? substringStoryboardName(withViewControllerName: controllerName)
if rootViewController,
let viewController = UIStoryboard(
name: String(storyboardName), bundle: nil
).instantiateInitialViewController() as? T {
return viewController
} else if let viewController = UIStoryboard(
name: String(storyboardName), bundle: nil
).instantiateViewController(withIdentifier: controllerName) as? T {
return viewController
} else {
fatalError("InstantiateInitialViewController not found")
}
}
private class func substringStoryboardName(withViewControllerName controllerName: String) -> String {
let viewControllerName = controllerName
if let range = viewControllerName.range(of: "ViewController") {
return String(viewControllerName[..<range.lowerBound])
} else {
return controllerName
}
}
}

View File

@ -0,0 +1,39 @@
//
// UITableView+Extensions.swift
//
//
// Created by okan.yucel on 6.03.2022.
//
import UIKit
public extension UITableView {
func reload(animation: Bool = true) {
DispatchQueue.main.async { [weak self] in
guard let `self` = self else { return }
guard animation else {
self.reloadData()
return
}
UIView.transition(
with: self, duration: 0.2, options: .transitionCrossDissolve, animations: { [weak self] in
self?.reloadData()
}, completion: nil
)
}
}
func registerNib(_ type: UITableViewCell.Type, bundle: Bundle) {
register(
UINib(nibName: type.identifier, bundle: bundle),
forCellReuseIdentifier: type.identifier
)
}
func dequeueCell<CellType: UITableViewCell>(type: CellType.Type, indexPath: IndexPath) -> CellType {
guard let cell = dequeueReusableCell(withIdentifier: CellType.identifier, for: indexPath) as? CellType else {
fatalError("Wrong type of cell \(type)")
}
return cell
}
}

View File

@ -0,0 +1,14 @@
//
// UIView+Extensions.swift
//
//
// Created by okan.yucel on 6.03.2022.
//
import UIKit
public extension UIView {
class var identifier: String {
String(describing: self)
}
}

View File

@ -0,0 +1,23 @@
//
// UIViewController+Extensions.swift
//
//
// Created by okan.yucel on 8.03.2022.
//
import UIKit
public protocol AlertPresentableLogic { }
public extension AlertPresentableLogic where Self: UIViewController {
func presentAlert(_ title: String, message: String, actionTitle: String, action: (() -> Void)? = nil) {
DispatchQueue.main.async {
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
alert.addAction(.init(title: actionTitle, style: .default, handler: { _ in
action?()
}))
self.present(alert, animated: true, completion: nil)
}
}
}

View File

@ -0,0 +1,93 @@
//
// UIViewController+BarButtonConfigurable.swift
//
//
// Created by okan.yucel on 6.03.2022.
//
import UIKit
public protocol BarButtonConfigurable: BarButtonItemConfiguration {}
/// Bar button position
public enum BarButtonItemPosition {
case right, left
}
/// Custom bar button type
public enum BarButtonItemType {
/// Pop view controller to back when selected.
case pop(BarButtonItemPosition)
/// Pop view controller to root when selected.
case popToRoot(BarButtonItemPosition)
/// Dismiss view controller selected.
case dismiss(BarButtonItemPosition)
/// custom button
case custom(BarButtonItemPosition, image: String, action: Selector)
}
public protocol BarButtonItemConfiguration: AnyObject {
func addBarButtonItem(ofType type: BarButtonItemType)
}
public extension BarButtonItemConfiguration where Self: UIViewController {
func addBarButtonItem(ofType type: BarButtonItemType) {
switch type {
case let .pop(position):
return newButton(
imageName: "back",
position: position,
action: #selector(Self.popController)
)
case let .popToRoot(position):
return newButton(
imageName: "close",
position: position,
action: #selector(Self.popToRootController)
)
case let .dismiss(position):
return newButton(
imageName: "close",
position: position,
action: #selector(Self.dismissController)
)
case .custom(let position, let image, let action):
return newButton(
imageName: image,
position: position,
action: action
)
}
}
func newButton(imageName: String, position: BarButtonItemPosition, action: Selector?) {
let button = UIBarButtonItem(
image: UIImage(named: imageName),
style: .plain,
target: self,
action: action
)
switch position {
case .left: navigationItem.leftBarButtonItem = button
case .right: navigationItem.rightBarButtonItem = button
}
if let image = button.image {
button.tintColor = navigationController?.navigationBar.tintColor ?? .darkText
button.image = image.withRenderingMode(.alwaysTemplate)
}
}
}
private extension UIViewController {
@objc func popController() {
navigationController?.popViewController(animated: true)
}
@objc func popToRootController() {
navigationController?.popToRootViewController(animated: true)
}
@objc func dismissController() {
dismiss(animated: true)
}
}

View File

@ -0,0 +1,11 @@
import XCTest
@testable import Extensions
final class ExtensionsTests: XCTestCase {
func testExample() throws {
// This is an example of a functional test case.
// Use XCTAssert and related functions to verify your tests produce the correct
// results.
XCTAssertEqual(Extensions().text, "Hello, World!")
}
}

21
README.md Normal file → Executable file
View File

@ -1,2 +1,21 @@
# CitieSearch
This project provides all requirements about Trie implementation on Swift.
First in first. You need to build all packages before building the project.
Packages:
- API
- Extensions
- Swiftrie
* Open API folder under project files and open Package.swift (You need to build it for iPhone)
* Open Extensions folder under project files and open Package.swift (You need to build it for iPhone)
* Open Swiftrie folder under project files and open Package.swift (You need to build it for iPhone)
* After you build them with successfully. Please open .xcodeproj of main project and build/run it.
## Swiftrie
Please, check [my solution](https://github.com/yucelokan/CitieSearch/blob/develop/Swiftrie/README.md).

7
Swiftrie/.gitignore vendored Normal file
View File

@ -0,0 +1,7 @@
.DS_Store
/.build
/Packages
/*.xcodeproj
xcuserdata/
DerivedData/
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata

View File

@ -0,0 +1,77 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1320"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "Swiftrie"
BuildableName = "Swiftrie"
BlueprintName = "Swiftrie"
ReferencedContainer = "container:">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "SwiftrieTests"
BuildableName = "SwiftrieTests"
BlueprintName = "SwiftrieTests"
ReferencedContainer = "container:">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "Swiftrie"
BuildableName = "Swiftrie"
BlueprintName = "Swiftrie"
ReferencedContainer = "container:">
</BuildableReference>
</MacroExpansion>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

27
Swiftrie/Package.swift Normal file
View File

@ -0,0 +1,27 @@
// swift-tools-version:5.5
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "Swiftrie",
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.
.library(
name: "Swiftrie",
targets: ["Swiftrie"])
],
dependencies: [
// Dependencies declare other packages that this package depends on.
],
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 this package depends on.
.target(
name: "Swiftrie",
dependencies: []),
.testTarget(
name: "SwiftrieTests",
dependencies: ["Swiftrie"])
]
)

47
Swiftrie/README.md Normal file
View File

@ -0,0 +1,47 @@
# Swiftrie
First thing first, what is a [Trie](https://en.wikipedia.org/wiki/Trie).?
> In computer science, a trie, also called digital tree or prefix tree, is a kind of search tree —an ordered tree data structure used to store a dynamic set or associative array where the keys are usually strings. [...] All the descendants of a node have a common prefix of the string associated with that node, and the root is associated with the empty string. Keys tend to be associated with leaves, though some inner nodes may correspond to keys of interest. Hence, keys are not necessarily associated with every node.
This library provides all requirements about Trie implementation on Swift.
What does this library offer:
* First of all everything; `Swiftrie works with Codables`, not only with words. Every final node stores a JSON string for itself. It means you can use it with Codables.
* Suit for all data models. Just implement `Swiftriable Interface` and that's all you need to make your model suit for Swiftrie.
* A new item can be `insertable`/`removable`.
* `Cancelable searches`. Swiftrie does it automatically. If you call the searching method again while the algorithm is searching a prefix, Swiftrie cancels the last search.
* Also you can add `throttle`/`delay` while searching in Swiftrie to prevent unnecessary searches. `Default is 0`. Because `Swiftrie can show results in 0.001 seconds in 209k data for an entered character`. Because Swiftrie does not wait for all nodes that will be visited. Check the following topic.
* Swiftrie provides showing results part by part. No need waiting for until the algorithm visits all nodes. It returns the results while visiting the nodes. To manage this logic just use `.gradually`. Default is `case indexable(_ index: 3)` index 3 means, the find method will return the response that it found at every 3 nodes without waiting for all nodes that will be visited.
* Everything in a `custom queue`. QoS is `.userInitiated`. And it is `concurrent`.
* Inserting and removing an item executes with `.barrier` in the custom queue. It means searching/getting will wait for inserting/removing and the algorithm will show correct results always.
* Unit tests have provided.
## Codes
```swift
let trie = Swiftrie(swiftriables: response.cities)
trie.gradually = .indexable(3)
trie.removeItem(/* a swiftriable item */)
trie.insertItem(/* a swiftriable item */)
let cities: [City] = trie.getAllItems()
trie.findItems(prefix: "istanbu", throttle: 0, type: City.self) { result in
print(result)
}
trie.cancelSearch()
```

View File

@ -0,0 +1,22 @@
//
// Codable+Extensions.swift
//
//
// Created by okan.yucel on 5.03.2022.
//
import Foundation
extension Encodable {
var data: Data? {
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
return try? encoder.encode(self)
}
var jsonString: String {
guard let data = data else { return "" }
guard let jsonString = String(data: data, encoding: .utf8) else { return "" }
return jsonString
}
}

View File

@ -0,0 +1,60 @@
//
// DispatchQueue+Extensions.swift
//
//
// Created by okan.yucel on 6.03.2022.
//
import Foundation
extension DispatchQueue {
/**
- parameters:
- target: Object used as the sentinel for de-duplication.
- delay: The time window for de-duplication to occur
- work: The work item to be invoked on the queue.
Performs work only once for the given target, given the time window. The last added work closure
is the work that will finally execute.
Note: This is currently only safe to call from the main thread.
Example usage:
```
DispatchQueue.main.asyncDeduped(target: self, after: 1.0) { [weak self] in
self?.doTheWork()
}
```
*/
func asyncDeduped(
target: AnyObject,
after delay: TimeInterval,
execute work: @escaping @convention(block) () -> Void
) {
let dedupeIdentifier = DispatchQueue.dedupeIdentifierFor(target)
if let existingWorkItem = DispatchQueue.workItems.removeValue(forKey: dedupeIdentifier) {
existingWorkItem.cancel()
}
let workItem = DispatchWorkItem {
DispatchQueue.workItems.removeValue(forKey: dedupeIdentifier)
for ptr in DispatchQueue.weakTargets.allObjects {
if dedupeIdentifier == DispatchQueue.dedupeIdentifierFor(ptr as AnyObject) {
work()
break
}
}
}
DispatchQueue.workItems[dedupeIdentifier] = workItem
DispatchQueue.weakTargets.addPointer(Unmanaged.passUnretained(target).toOpaque())
asyncAfter(deadline: .now() + delay, execute: workItem)
}
}
// MARK: - Static Properties for De-Duping
private extension DispatchQueue {
static var workItems = [AnyHashable: DispatchWorkItem]()
static var weakTargets = NSPointerArray.weakObjects()
static func dedupeIdentifierFor(_ object: AnyObject) -> String {
"\(Unmanaged.passUnretained(object).toOpaque())." + String(describing: object)
}
}

View File

@ -0,0 +1,14 @@
//
// String+Extensions.swift
//
//
// Created by okan.yucel on 5.03.2022.
//
import Foundation
extension String {
func toObject<T: Decodable>() -> T? {
return try? JSONDecoder().decode(T.self, from: Data(self.utf8))
}
}

View File

@ -0,0 +1,63 @@
//
// YOOrderedDictionary.swift
//
//
// Created by okan.yucel on 12.04.2022.
//
import Foundation
class YOOrderedDictionary<Key: Hashable, Value> {
typealias Element = (key: Key, value: Value)
init() { }
private (set) var values: [Element] = []
subscript(key: Key) -> Value? {
get {
guard let item = values.first(where: {$0.key == key}) else { return nil }
return item.value
}
set(newValue) {
guard let value = newValue else {
// if it is nil, remove it
values.removeAll(where: {$0.key == key})
return
}
let element = (key: key, value: value)
guard let index = values.firstIndex(where: {$0.key == key}) else {
// if there is no exist, append it
values.append(element)
return
}
// if it already exists, remove it and insert it
values.remove(at: index)
values.insert(element, at: index)
}
}
func sort(
by condition: (Element, Element) -> Bool
) {
values.sort(by: condition)
}
var first: Value? {
return values.first?.value
}
var last: Value? {
return values.last?.value
}
var count: Int {
return values.count
}
var isEmpty: Bool {
return values.isEmpty
}
}

View File

@ -0,0 +1,12 @@
//
// SwiftrieItem.swift
//
//
// Created by okan.yucel on 5.03.2022.
//
import Foundation
public protocol Swiftriable: Codable {
var prefixableText: String { get }
}

View File

@ -0,0 +1,59 @@
//
// File.swift
//
//
// Created by okan.yucel on 7.03.2022.
//
import Foundation
class SwiftriableFindPrimes<T: Swiftriable>: Operation {
typealias PrimesTrie = SwiftriableLogic & SwiftrieFindableLogic & SwiftrieStorableLogic
init(swiftriable: PrimesTrie,
prefix: String,
type: T.Type,
completion: @escaping ([T]) -> Void) {
self.swiftriable = swiftriable
self.prefix = prefix
self.type = type
self.completion = completion
}
private unowned var swiftriable: PrimesTrie
private var prefix: String
private var type: T.Type
private var completion: ([T]) -> Void
override func main() {
findResults(with: prefix, type: type, completion: completion)
}
private func findResults<T: Swiftriable>(
with prefix: String, type: T.Type? = nil, completion: (([T]) -> Void)? = nil
) {
let graduallyIndex = (
swiftriable as? SwiftrieAccessibleLogic
)?.gradually.modIndex ?? -1
var items: [T] = []
let prefixLowerCased = prefix.lowercased()
if let lastNode = swiftriable.findLastNodeOf(word: prefixLowerCased) {
if lastNode.isFinal {
let subItems: [T] = lastNode.items.compactMap({$0.toObject()})
items.append(contentsOf: subItems)
}
for item in lastNode.childrens.values.enumerated() {
if isCancelled {
return
}
let childItems: [T] = swiftriable.itemsInSubtrie(rootNode: item.element.value, partialWord: prefixLowerCased)
items += childItems
if graduallyIndex > 1, item.offset%graduallyIndex == 0 {
swiftriable.queue.async {
completion?(items)
}
}
}
}
completion?(items)
}
}

View File

@ -0,0 +1,23 @@
//
// SwiftriableGraduallyLogic.swift
//
//
// Created by okan.yucel on 8.03.2022.
//
import Foundation
public enum SwiftriableGraduallyLogic {
case nonIndexable
case indexable(_ index: Int)
var modIndex: Int? {
switch self {
case .nonIndexable:
return nil
case .indexable(let index):
guard index > 1 else { return 2 }
return index
}
}
}

View File

@ -0,0 +1,38 @@
//
// Swiftrie+SwiftriableLogic.swift
//
//
// Created by okan.yucel on 6.03.2022.
//
import Foundation
protocol SwiftriableLogic: AnyObject {
init(swiftriables: [Swiftriable])
func items<T: Swiftriable>() -> [T]
func find<T: Swiftriable>(
with prefix: String, type: T.Type, completion: @escaping (([T]) -> Void)
)
}
extension SwiftriableLogic where Self: SwiftrieFindableLogic & SwiftrieStorableLogic {
/// All Swiftriables currently in the trie
func items<T: Swiftriable>() -> [T] {
return itemsInSubtrie(rootNode: root, partialWord: "")
}
/// Fetchs part by part an array of Swiftriable in a subtrie of the trie that start with the given prefix
/// - Parameters:
/// - prefix: the letters for word prefix
/// - type: type of Swiftriable
/// - completion: Swiftriables in the subtrie that start with prefix (party by part)
func find<T: Swiftriable>(
with prefix: String, type: T.Type, completion: @escaping (([T]) -> Void)
) {
operations.cancelAllOperations()
let operation = SwiftriableFindPrimes<T>(
swiftriable: self, prefix: prefix, type: type, completion: completion
)
operations.addOperation(operation)
}
}

View File

@ -0,0 +1,22 @@
//
// SwiftrieNode+SwiftriableModifiableNodeLogic.swift
//
//
// Created by okan.yucel on 6.03.2022.
//
import Foundation
protocol SwifriableModifiableNodeLogic: AnyObject {
init(value: Character?, parent: SwiftriableNode?, item: String?)
func add(value: Character, item: String?)
}
extension SwifriableModifiableNodeLogic where Self: SwiftriableStorableNodeLogic {
/// Adds a child node to self.
/// - Parameter value: The item to be added to this node.
func add(value: Character, item: String? = nil) {
childrens[value] = SwiftrieNode(value: value, parent: self, item: item)
childrens.sort(by: {$0.key.lowercased() < $1.key.lowercased()})
}
}

View File

@ -0,0 +1,17 @@
//
// SwiftrieNode+SwiftriableStorableNodeLogic.swift
//
//
// Created by okan.yucel on 6.03.2022.
//
import Foundation
protocol SwiftriableStorableNodeLogic: AnyObject {
var value: Character? { get set }
var items: [String] { get set }
var parent: SwiftriableNode? { get set }
var childrens: YOOrderedDictionary<Character, SwiftriableNode> { get set }
var isFinal: Bool { get set }
var isLeaf: Bool { get }
}

View File

@ -0,0 +1,26 @@
//
// Swiftrie.swift
//
//
// Created by okan.yucel on 6.03.2022.
//
import Foundation
typealias SwiftrieAllLogics = (
SwiftriableLogic & SwiftrieStorableLogic & SwiftrieFindableLogic & SwiftrieModifiableLogic
)
public class Swiftrie: SwiftrieAllLogics {
required public init(swiftriables: [Swiftriable]) {
swiftriables.forEach { swiftriable in
insert(swiftriable)
}
}
var root: SwiftriableNode = SwiftrieNode(value: nil, parent: nil, item: nil)
var operations: OperationQueue = OperationQueue()
var queue: DispatchQueue = DispatchQueue(label: "swiftrie-safe", qos: .userInitiated, attributes: .concurrent)
public var gradually: SwiftriableGraduallyLogic = .indexable(3)
}

View File

@ -0,0 +1,76 @@
//
// SwiftrieAccessibleLogic.swift
//
//
// Created by okan.yucel on 8.03.2022.
//
import Foundation
public protocol SwiftrieAccessibleLogic {
/// cancel all search thats are active
func cancelSearch()
/// get all items in Swiftrie.
/// - Returns: A Swiftriable array that is generic. Response type should be provided.
func getAllItems<T: Swiftriable>() -> [T]
/// Fetchs part by part an array of Swiftriable in a subtrie of the trie that start with the given prefix
/// - Parameters:
/// - prefix: the letters for word prefix
/// - throttle: delay for search / deduped
/// - type: type of Swiftriable
/// - completion: Swiftriables in the subtrie
/// that start with prefix (party by part) (to change logic check .gradually)
func findItems<T: Swiftriable>(
prefix: String, throttle: Double, type: T.Type, completion: @escaping (([T]) -> Void)
)
/// remove a Swiftriable item from Swiftrie .
/// - Parameter Swiftriable: the Swiftriable to be removed
func removeItem(_ swiftriable: Swiftriable)
/// Inserts a Swiftriable into the trie.
/// - Parameter Swiftriable: the Swiftriable to be inserted.
func insertItem(_ swiftriable: Swiftriable)
/// a logic to get data part by part. Default is `case indexable(_ index: 3)`
/// index 3 means, the find method will return the response
/// that it found at every 3 nodes without waiting for all nodes that will be visited.
var gradually: SwiftriableGraduallyLogic { get set }
}
extension Swiftrie: SwiftrieAccessibleLogic {
public func cancelSearch() {
operations.cancelAllOperations()
}
public func getAllItems<T: Swiftriable>() -> [T] {
queue.sync {
return items()
}
}
public func findItems<T: Swiftriable>(
prefix: String, throttle: Double, type: T.Type, completion: @escaping (([T]) -> Void)
) {
queue.asyncDeduped(target: self, after: throttle) { [weak self] in
self?.queue.sync {
self?.find(with: prefix, type: type, completion: completion)
}
}
}
public func removeItem(_ swiftriable: Swiftriable) {
queue.async(flags: .barrier) {
self.remove(swiftriable)
}
}
public func insertItem(_ swiftriable: Swiftriable) {
queue.async(flags: .barrier) {
self.insert(swiftriable)
}
}
}

View File

@ -0,0 +1,90 @@
//
// Swiftrie+Extensions.swift
//
//
// Created by okan.yucel on 5.03.2022.
//
import Foundation
protocol SwiftrieFindableLogic: AnyObject {
func findLastNodeOf(word: String) -> SwiftriableNode?
func findTerminalNodeOf(word: String) -> SwiftriableNode?
func deleteNodesForWordEndingWith(terminalNode: SwiftriableNode)
func itemsInSubtrie<T: Swiftriable>(rootNode: SwiftriableNode, partialWord: String) -> [T]
}
extension SwiftrieFindableLogic where Self: SwiftrieStorableLogic {
/// Attempts to walk to the last node of a word. The
/// search will fail if the word is not present. Doesn't
/// check if the node is terminating
///
/// - Parameter word: the word in question
/// - Returns: the node where the search ended, nil if the
/// search failed.
func findLastNodeOf(word: String) -> SwiftriableNode? {
var currentNode = root
for character in word.lowercased() {
guard let childNode = currentNode.childrens[character] else {
return nil
}
currentNode = childNode
}
return currentNode
}
/// Attempts to walk to the terminating node of a word. The
/// search will fail if the word is not present.
///
/// - Parameter word: the word in question
/// - Returns: the node where the search ended, nil if the
/// search failed.
func findTerminalNodeOf(word: String) -> SwiftriableNode? {
if let lastNode = findLastNodeOf(word: word) {
return lastNode.isFinal ? lastNode : nil
}
return nil
}
/// Deletes a word from the trie by starting with the last letter
/// and moving back, deleting nodes until either a non-leaf or a
/// terminating node is found.
///
/// - Parameter terminalNode: the node representing the last node
/// of a word
func deleteNodesForWordEndingWith(terminalNode: SwiftriableNode) {
var lastNode = terminalNode
var character = lastNode.value
while lastNode.isLeaf, let parentNode = lastNode.parent {
lastNode = parentNode
lastNode.childrens[character ?? Character("empty")] = nil
character = lastNode.value
if lastNode.isFinal {
break
}
}
}
/// Returns an array of words in a subtrie of the trie
///
/// - Parameters:
/// - rootNode: the root node of the subtrie
/// - partialWord: the letters collected by traversing to this node
/// - Returns: the objects in the subtrie
func itemsInSubtrie<T: Swiftriable>(rootNode: SwiftriableNode, partialWord: String) -> [T] {
var subtrieItems: [T] = []
var previousLetters = partialWord
if let value = rootNode.value {
previousLetters.append(value)
}
if rootNode.isFinal {
let items: [T] = rootNode.items.compactMap({$0.toObject()})
subtrieItems.append(contentsOf: items)
}
for item in rootNode.childrens.values {
let childItems: [T] = itemsInSubtrie(rootNode: item.value, partialWord: previousLetters)
subtrieItems += childItems
}
return subtrieItems
}
}

View File

@ -0,0 +1,74 @@
//
// Swiftrie+SwiftrieModifiableLogic.swift
//
//
// Created by okan.yucel on 6.03.2022.
//
import Foundation
protocol SwiftrieModifiableLogic {
func remove(_ swiftriable: Swiftriable)
func insert(_ swiftriable: Swiftriable)
}
extension SwiftrieModifiableLogic where Self: SwiftrieFindableLogic & SwiftrieStorableLogic {
/// Inserts a Swiftriable into the trie.
/// - Parameter Swiftriable: the Swiftriable to be inserted.
func insert(_ swiftriable: Swiftriable) {
guard !swiftriable.prefixableText.isEmpty else { return }
var currentNode = root
swiftriable.prefixableText.lowercased().forEach { character in
if let childNode = currentNode.childrens[character] {
currentNode = childNode
} else {
currentNode.add(value: character)
if let childNode = currentNode.childrens[character] {
currentNode = childNode
}
}
}
// Word already present?
guard !currentNode.isFinal else {
jsonAppender(node: currentNode, item: swiftriable)
return
}
currentNode.isFinal = true
jsonAppender(node: currentNode, item: swiftriable)
}
/// Removes a Swiftriable from the trie. If the Swiftriable is not present or
/// it is empty, just ignore it. If the last node is a leaf,
/// and this node has a only an item as JSON string
/// delete that node and higher nodes that are leaves until a
/// terminating node or non-leaf is found.
/// If it has more then one item only remove JSON string
/// - Parameter Swiftriable: the Swiftriable to be removed.
func remove(_ swiftriable: Swiftriable) {
guard !swiftriable.prefixableText.isEmpty,
let terminalNode = findTerminalNodeOf(word: swiftriable.prefixableText) else { return }
guard terminalNode.isLeaf else {
terminalNode.isFinal = false
return
}
if terminalNode.items.count > 1 {
if let index = terminalNode.items.firstIndex(where: {$0 == swiftriable.jsonString}) {
terminalNode.items.remove(at: index)
}
} else {
deleteNodesForWordEndingWith(terminalNode: terminalNode)
}
}
/// add the json if it doesn't exist
private func jsonAppender(node: SwiftriableNode, item: Swiftriable) {
let jsonString = item.jsonString
if !node.items.contains(jsonString) {
node.items.append(jsonString)
}
}
}

View File

@ -0,0 +1,36 @@
//
// SwiftrieNode.swift
//
//
// Created by okan.yucel on 5.03.2022.
//
import Foundation
typealias SwiftriableNode = SwifriableModifiableNodeLogic & SwiftriableStorableNodeLogic
/// A node in the trie
class SwiftrieNode: SwiftriableNode {
/// Initializes a node.
///
/// - Parameters:
/// - value: The value that goes into the node
/// - parent: A reference to this node's parent
/// - item: a json string for the swiftriable item
required init(value: Character?, parent: SwiftriableNode?, item: String?) {
self.value = value
self.parent = parent
self.items.append(item ?? "")
}
var value: Character?
var items: [String] = []
weak var parent: SwiftriableNode?
var childrens: YOOrderedDictionary<Character, SwiftriableNode> = .init()
var isFinal: Bool = false
var isLeaf: Bool {
return childrens.isEmpty
}
}

View File

@ -0,0 +1,14 @@
//
// SwiftrieStorableLogic.swift
//
//
// Created by okan.yucel on 8.03.2022.
//
import Foundation
protocol SwiftrieStorableLogic: AnyObject {
var root: SwiftriableNode { get set }
var operations: OperationQueue { get set }
var queue: DispatchQueue { get set }
}

Some files were not shown because too many files have changed in this diff Show More