- Features
- GRCompatible
- GoodCache
- GoodExtensions
- Array
- CGAffineTransforms
- Data
- Date
- UICollectionView TableView
- UITableView
- Storyboard
- Lossy Codable Array
- MKMultiPoint
- Attributed string
- NSCollectionLayoutGroup
- CGAffineTransforms
- NameDescribable
- String
- UIAlertController
- UICollectionViewCell
- UIColor
- UIDatePicker
- UIDevice
- UILabel
- UINavigationController
- UIScrollView
- UIView
- URL
- UIViewController
- GoodCombineExtensions
- GoodReactor
- GoodRequestManager
- GoodStructs
- Installation
- License
GoodIOSExtensions is a collection of modules extending different aspects of your swift xcode project
- GRCompatible
- GoodCache
- GoodCombineExtensions
- GoodExtensions
- GoodReactor
- GoodRequestManager
- GoodStructs
A wrapper protocol for distinguishing our extensions. To call goodrequest extensions you refer to .gr ie:
cartButton.gr.tapPublisher
A property wrapper to make caching into keychain and UserDefaults extremely easy.
@UserDefaultValue("appState", defaultValue: .inactive)
var appState: AppState
@KeychainValue("accessToken", defaultValue: "", accessibility: .afterFirstUnlockThisDeviceOnly)
var accessToken: String
All the other extensions:
Returns array of elements where between each element will be inserted element, provided in the parameter.
enum CellType {
case section(String)
case separator
}
let array: [CellType] = [
.section("One"),
.section("Two"),
.section("Three")
].gr.separated(by: .separator)
Output
array: .section("One"), .separator, .section("Two"), .separator, .section("Three")
Returns true if array contains item with specified index, otherwise returns false.
[1, 2, 3, 4].contains(index: 5)
[1, 2, 3, 4].contains(index: 4)
Output
FALSE
TRUE
Returns if Collection has any elements
[1, 2, 3, 4].hasItems
[].hasItems
Output
TRUE
FALSE
Functions for collection operations:
- removingOrAppending
- joinNonNil
- chunked
- removedDuplicates
- prepending
- swapped
Safely ask for item at index using the safe subscript
Create a transform with scale, translation and anchor in place with the create function
Creates string from hex data format hexString
A simple function to add time value into an existing date with the adding function
Set of functions to register and dequeue cells
Header updating functions to recalculate content with the sizeHeaderToFit, updateHeaderWidth and sizeFooterToFit
Instantiate storyboard from view controller typename with the instantiateViewController function
Property wrapper that does compact map on top of an array value. Default empty array
@LossyCodableArray<Widget> var widgets: [Widget]
Creates an array of MKMapPints from MKMultiPoint with the points property
Contains functions to create Attributed text from HTML string and functions to work with NSMUtableAttributedString
Create a horizontal layout group for Compositional Layout with the horizontalWithDimensions function
Extracts typename from Collection, NSObject or Enum
public protocol NameDescribable {
var typeName: String { get }
static var typeName: String { get }
}
Removes diacritics from string with the removeDiacritics
Create alert menu to open Coordinates via different maps with the create function
get status bar frame with currentStatusBarFrame property
open URL of Type with predefines URLType
public enum UIApplicationUrlType {
case instagramMedia(id: String)
case telepromt(number: String)
case settings
}
using the open function or just safely open a standard URL with the safeOpen function
Animate cell selection shrinking it when selected for 0.2 seconds with the animate
Create UIColor from 3 equal RGB values or try parse color from hex with our color functions
dateBinding computed property for observing datepicker values
get info about the device with deviceId, deviceSystem, deviceName and deviceType
Computed property isTruncated checks if intrisic with is wider than bounds
Push into navigation view controller with completion with the pushViewController function
Computed property isRefreshing cheecks if any refreshing controll is available check if its refreshing
Nib loading for initialization through constructor with the loadNib function A list of IBInspectable attributes for UIView
- cornerRadius
- borderColor
- borderWidth
- masksToBounds
- shadowOpacity
- shadowColor
- shadowRadius
- shadowOffset
Shake the view repeatedly with the shakeView Rotate view by given Rotate Options
enum Rotate {
case by0
case by90
case by180
case by270
case custom(Double)
}
with the rotate
Animate view fading with the animate function
Clip corner radius to exact half with the circleMaskImage function Blur view beautifuly blur and unblur with the blur and unblur functions.
formatted computed property returns URL formatted as follows "(scheme)://(host)" or returns absolute url string
Embed view controller into container with the embed function or make instance of viewController with the makeInstance function
Extends the combine framework by some convenient events that help you build a reactive app
A publisher for tapping the UIControll items.
Click to expand!
lazy var buttonPublisher = showAllMatchesButton.gr.publisher(for: .touchUpInside)
.mapToVoid()
.erased()
A publisher for tapping the bar button items.
Click to expand!
sortButton.gr.tapPublisher
.sink { [weak self] _ in
guard let self = self else { return }
let controller = self.createPickerViewController(
with: SortValues.allCases,
preselectedItems: self.viewModel.preselectedSortPickerItems
)
self.present(controller, animated: true)
}
.store(in: &cancellables)
Contains an event publisher much like UIControll and BarButtonItem
Offers a more legible option for chaing multiple Publishers
Assign operator alows you to set key in the given object path
Nwise combine operator for when native operators aren't enough
Goodreactor is an adaptation of the Reactor framework that is Redux inspired. The view model communicates with the view controller via the State and with the Coordinator via the navigation function. You communicate to the viewModel via Actions Viewmodel changes state in the Reduce function Viewmodel interactes with dependencies outside of the Reduce function not to create side-effects
Link to the original reactor kit: https://github.com/ReactorKit/ReactorKit
Click to expand!
import Foundation
import Combine
// MARK: - View Model Implementation
final class LoginViewModel: Reactor {
// MARK: - Typealiases
typealias LoginResult = GRResult<Bool, AppError>
// MARK: - View Model Definitions
struct State {
var loginResult: LoginResult?
}
enum Action {
case loginUser(AuthorizeRequest)
case goToRegistration
}
enum Mutation {
case loginResultChanged(LoginResult)
}
// MARK: - Constants
internal let initialState: State
internal let coordinator: Coordinator<AppStep>
// MARK: - Initialization
init(di: DI, coordinator: Coordinator<AppStep>) {
self.coordinator = coordinator
self.initialState = State(loginResult: nil)
}
}
// MARK: - Reactive
extension LoginViewModel {
func navigate(action: Action) -> AppStep? {
switch action {
case .goToRegistration:
return .loginStep(.goToRegistration)
case .loginUser:
return nil
}
}
func mutate(action: Action) -> AnyPublisher<Mutation, Never> {
switch action {
case .loginUser(let authorizeRequest):
return loginUser(
authorizeRequest: authorizeRequest,
requestManager: di.requestManager,
userRequestManager: di.userRequestManager,
cache: di.cache
)
case .goToRegistration:
return Empty().erased()
}
}
func reduce(state: State, mutation: Mutation) -> State {
var state = state
switch mutation {
case .loginResultChanged(let result):
state.loginResult = result
}
return state
}
}
// MARK: - Private
private extension LoginViewModel {
func loginUser(
authorizeRequest: AuthorizeRequest,
requestManager: RequestManagerType,
userRequestManager: UserRequestManagerType,
cache: CacheType
) -> AnyPublisher<Mutation, Never> {
return requestManager.loginUser(authorizeRequest: authorizeRequest)
.handleEvents(receiveOutput: { cache.cache(accessToken: $0.accessToken) })
.flatMap { _ in
userRequestManager.loadWalkthroughState()
.map { Mutation.loginResultChanged(.success($0.walkthroughPassed ?? false)) }
.mapError { AppError.af($0) }
}
.prepend(.loginResultChanged(.loading))
.catch { Just(Mutation.loginResultChanged(.failure($0))) }
.erased()
}
}
With a login viewModel like this you receive values in the viewController binding yourself like this.
func bindState(reactor: LoginViewModel) {
reactor.state
.map { $0.loginResult }
.removeDuplicates()
.compactMap { $0 }
.sink(receiveValue: showLoginResult)
.store(in: &cancellables)
}
And the coordinator navigation looks like this
import UIKit
import Combine
// MARK: - Steps
enum LoginStep {
case goToRegistration
case goToLogin
}
final class LoginCoordinator: Coordinator<AppStep> {
// MARK: - Properties
private let parentCoordinator: Coordinator<AppStep>
// MARK: - Initialization
init(
parentCoordinator: Coordinator<AppStep>
) {
self.parentCoordinator = parentCoordinator
}
// MARK: - Overrides
override func navigate(to step: AppStep) -> StepAction {
switch step {
case .loginStep(let loginStep):
return navigate(to: loginStep)
default:
return .none
}
}
@discardableResult
override func start() -> UIViewController? {
super.start()
navigationController = UINavigationController()
setupInitialController()
let viewController = UIViewController()
navigationController?.viewControllers = [initialController]
return navigationController
}
}
// MARK: - Navigation
private extension LoginCoordinator {
func navigate(to step: LoginStep) -> StepAction {
switch step {
case .goToRegistration:
let registerViewModel = RegisterViewModel(di: di, coordinator: self)
let registerViewController = RegisterViewController.create(viewModel: registerViewModel)
return .push(registerViewController)
case .goToLogin:
return .push(createLoginViewController())
}
}
}
GoodCoordinator also allows you to find the first type matching coordinator in hierarchy via: firstCoordinatorOfType and lastCoordinatorOfType functions
Contains our GRSession that works with GREndpointManager and GRCodable and DataRequestExtensions.
Click to expand!
import Foundation
import Alamofire
import Combine
enum UserRequestEndpoint: GREndpointManager {
// MARK: - User Profile
case profile
var path: String {
switch self {
case .profile,
return "v1/me/profile"
}
}
var method: HTTPMethod {
switch self {
case .profile,
return .get
}
}
var queryParameters: Either<Parameters, GREncodable>? {
return nil
}
var parameters: Either<Parameters, GREncodable>? {
return nil
}
var encoding: ParameterEncoding {
return method == .get ? URLEncoding(destination: .methodDependent) : JSONEncoding.default
}
var headers: HTTPHeaders? {
return [.contentType("application/json")]
}
func asURL(baseURL: String) throws -> URL {
var url = try baseURL.asURL()
url.appendPathComponent(path)
return url
}
}
class UserRequestManager: UserRequestManagerType {
// MARK: - Constants
internal let session: GRSession<UserRequestEndpoint, ApiServer>
internal let cache: CacheType
// MARK: - Initialization
init(baseURL: String, cache: CacheType) {
session = GRSession(
configuration: .default,
baseURL: baseURL,
interceptor: RequestInterceptor(cache: cache)
)
self.cache = cache
}
// MARK: - User Profile
func fetchProfile() -> AnyPublisher<ProfileResponse, AFError> {
return session.request(endpoint: .profile)
.validateToCustomError()
.goodify()
}
}
And then inside your viewModel just call
fetchProfile(requestManager: requestManager, id: id, appSpace: appSpace),
The result is a publisher so you can continue chaining Combine functions.
Also contains datarequest logger that logs the payload and response and request url of sent datarequests to be able to debug it more easily
Either represents a value of one of two possible types (a disjoint union).
Click to expand!
var queryParameters: Either<Parameters, GREncodable>? {
switch self {
case .login:
return .left(
[
"client_id": Environment.isProductionAppId,
"appSpace": AppSpace.defaultAppSpace
]
)
default:
return nil
}
}
Makes it available to set properties with closures just after initializing.
Click to expand!
surnameLabel.then {
$0.font = AC.DynamicFont.largeTitle
$0.textColor = Color.blueDark.color
}
Empty codable equatable and error struct
Click to expand!
func resetPassword(
requestManager: RequestManagerType,
email: String
) -> AnyPublisher<Mutation, Never> {
return requestManager.resetPassword(email: email)
.mapError { AppError.af($0) }
.map { _ in Mutation.resetPasswordResultChanged(.success(Nothing())) }
.prepend(.resetPasswordResultChanged(.loading))
.catch { Just(Mutation.resetPasswordResultChanged(.failure($0))) }
.erased()
}
Create a Package.swift
file and add the package dependency into the dependencies list.
Or to integrate without package.swift add it through the Xcode add package interface.
import PackageDescription
let package = Package(
name: "SampleProject",
dependencies: [
.Package(url: "https://github.com/GoodRequest/GoodIOSExtensions" from: "0.2.3")
]
)
GoodIOSExtensions repository is released under the MIT license. See LICENSE for details.