latest version
This commit is contained in:
parent
d9d2d25141
commit
3ad434f219
|
@ -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
|
||||
|
|
@ -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
|
|
@ -0,0 +1,7 @@
|
|||
.DS_Store
|
||||
/.build
|
||||
/Packages
|
||||
/*.xcodeproj
|
||||
xcuserdata/
|
||||
DerivedData/
|
||||
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
|
|
@ -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>
|
|
@ -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"])
|
||||
]
|
||||
)
|
|
@ -0,0 +1,3 @@
|
|||
# API
|
||||
|
||||
A description of this package.
|
|
@ -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?
|
||||
}
|
|
@ -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 {}
|
|
@ -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
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "self:">
|
||||
</FileRef>
|
||||
</Workspace>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"colors" : [
|
||||
{
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
|
@ -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 |
|
@ -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 |
|
@ -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>
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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: "")
|
||||
}
|
||||
}
|
|
@ -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)")
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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>
|
|
@ -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))
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
|
@ -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?
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
//
|
||||
// CityMapWorker.swift
|
||||
// CitieSearch
|
||||
//
|
||||
// Created by okan.yucel on 6.03.2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
protocol CityMapWorkingLogic: AnyObject {
|
||||
|
||||
}
|
||||
|
||||
final class CityMapWorker: CityMapWorkingLogic {
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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>
|
|
@ -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))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
|
@ -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))
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
|
@ -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 ?? "")
|
||||
)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
//
|
||||
// CitySearchWorker.swift
|
||||
// CitieSearch
|
||||
//
|
||||
// Created by okan.yucel on 5.03.2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
protocol CitySearchWorkingLogic: AnyObject {
|
||||
|
||||
}
|
||||
|
||||
final class CitySearchWorker: CitySearchWorkingLogic {
|
||||
|
||||
}
|
|
@ -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>
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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)")
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
|
@ -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.";
|
|
@ -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.";
|
|
@ -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.
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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 it’s 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
.DS_Store
|
||||
/.build
|
||||
/Packages
|
||||
/*.xcodeproj
|
||||
xcuserdata/
|
||||
DerivedData/
|
||||
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
|
|
@ -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>
|
|
@ -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"])
|
||||
]
|
||||
)
|
|
@ -0,0 +1,3 @@
|
|||
# Extensions
|
||||
|
||||
A description of this package.
|
|
@ -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")
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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!")
|
||||
}
|
||||
}
|
|
@ -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).
|
||||
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
.DS_Store
|
||||
/.build
|
||||
/Packages
|
||||
/*.xcodeproj
|
||||
xcuserdata/
|
||||
DerivedData/
|
||||
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
|
|
@ -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>
|
|
@ -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"])
|
||||
]
|
||||
)
|
|
@ -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()
|
||||
```
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
//
|
||||
// SwiftrieItem.swift
|
||||
//
|
||||
//
|
||||
// Created by okan.yucel on 5.03.2022.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public protocol Swiftriable: Codable {
|
||||
var prefixableText: String { get }
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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()})
|
||||
}
|
||||
}
|
|
@ -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 }
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
Loading…
Reference in New Issue