mirror of
https://repo.dactyloidae.xyz/Dactyloidae/UXP.git
synced 2026-09-05 15:28:39 +09:00
Dactyloidae iOS initial commit
This commit is contained in:
parent
daa6179d22
commit
7154a0497e
2123 changed files with 197052 additions and 0 deletions
42
mobile/ios/Client/Extensions/GeometryExtensions.swift
Normal file
42
mobile/ios/Client/Extensions/GeometryExtensions.swift
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import UIKit
|
||||
|
||||
extension CGRect {
|
||||
var center: CGPoint {
|
||||
get {
|
||||
return CGPoint(x: size.width / 2, y: size.height / 2)
|
||||
}
|
||||
set {
|
||||
self.origin = CGPoint(x: newValue.x - size.width / 2, y: newValue.y - size.height / 2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension UIEdgeInsets {
|
||||
init(equalInset inset: CGFloat) {
|
||||
top = inset
|
||||
left = inset
|
||||
right = inset
|
||||
bottom = inset
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
Generates the affine transform for transforming the first CGRect into the second one
|
||||
|
||||
- parameter frame: CGRect to transform from
|
||||
- parameter toFrame: CGRect to transform to
|
||||
|
||||
- returns: CGAffineTransform that transforms the first CGRect into the second
|
||||
*/
|
||||
func CGAffineTransformMakeRectToRect(_ frame: CGRect, toFrame: CGRect) -> CGAffineTransform {
|
||||
let scale = toFrame.size.width / frame.size.width
|
||||
let tx = toFrame.origin.x + toFrame.width / 2 - (frame.origin.x + frame.width / 2)
|
||||
let ty = toFrame.origin.y - frame.origin.y * scale * 2
|
||||
let translation = CGAffineTransform(translationX: tx, y: ty)
|
||||
let scaledAndTranslated = translation.scaledBy(x: scale, y: scale)
|
||||
return scaledAndTranslated
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import Foundation
|
||||
|
||||
// MARK: - Common UITableView text styling
|
||||
extension NSAttributedString {
|
||||
static func tableRowTitle(_ string: String, enabled: Bool) -> NSAttributedString {
|
||||
let color = enabled ? [NSForegroundColorAttributeName: UIConstants.TableViewRowTextColor] : [NSForegroundColorAttributeName: UIConstants.TableViewDisabledRowTextColor]
|
||||
return NSAttributedString(string: string, attributes: color)
|
||||
}
|
||||
}
|
||||
55
mobile/ios/Client/Extensions/NSURLExtensionsMailTo.swift
Normal file
55
mobile/ios/Client/Extensions/NSURLExtensionsMailTo.swift
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import Foundation
|
||||
|
||||
/**
|
||||
* Data structure containing metadata associated with a mailto: link. For additional details,
|
||||
* see RFC 2368 https://tools.ietf.org/html/rfc2368
|
||||
*/
|
||||
public struct MailToMetadata {
|
||||
public let to: String
|
||||
public let headers: [String: String]
|
||||
}
|
||||
|
||||
public extension URL {
|
||||
|
||||
/**
|
||||
Extracts the metadata associated with a mailto: URL according to RFC 2368
|
||||
https://tools.ietf.org/html/rfc2368
|
||||
*/
|
||||
func mailToMetadata() -> MailToMetadata? {
|
||||
guard scheme == "mailto" else {
|
||||
return nil
|
||||
}
|
||||
let urlString = absoluteString
|
||||
|
||||
// Extract 'to' value
|
||||
let toStart = urlString.characters.index(urlString.startIndex, offsetBy: "mailto:".characters.count)
|
||||
let toEnd = urlString.characters.index(of: "?") ?? urlString.endIndex
|
||||
|
||||
let to = urlString.substring(with: toStart..<toEnd)
|
||||
|
||||
guard toEnd != urlString.endIndex else {
|
||||
return MailToMetadata(to: to, headers: [String: String]())
|
||||
}
|
||||
|
||||
// Extract headers
|
||||
let headersString = urlString.substring(with: urlString.index(toEnd, offsetBy: 1)..<urlString.endIndex)
|
||||
var headers = [String: String]()
|
||||
let headerComponents = headersString.components(separatedBy: "&")
|
||||
|
||||
headerComponents.forEach { headerPair in
|
||||
let components = headerPair.components(separatedBy: "=")
|
||||
guard components.count == 2 else {
|
||||
return
|
||||
}
|
||||
|
||||
let (hname, hvalue) = (components[0], components[1])
|
||||
headers[hname] = hvalue
|
||||
}
|
||||
|
||||
return MailToMetadata(to: to, headers: headers)
|
||||
}
|
||||
}
|
||||
190
mobile/ios/Client/Extensions/UIAlertControllerExtensions.swift
Normal file
190
mobile/ios/Client/Extensions/UIAlertControllerExtensions.swift
Normal file
|
|
@ -0,0 +1,190 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import UIKit
|
||||
|
||||
typealias UIAlertActionCallback = (UIAlertAction) -> Void
|
||||
|
||||
// MARK: - Extension methods for building specific UIAlertController instances used across the app
|
||||
extension UIAlertController {
|
||||
|
||||
/**
|
||||
Builds the Alert view that asks the user if they wish to opt into crash reporting.
|
||||
|
||||
- parameter sendReportCallback: Send report option handler
|
||||
- parameter alwaysSendCallback: Always send option handler
|
||||
- parameter dontSendCallback: Dont send option handler
|
||||
- parameter neverSendCallback: Never send option handler
|
||||
|
||||
- returns: UIAlertController for opting into crash reporting after a crash occurred
|
||||
*/
|
||||
class func crashOptInAlert(
|
||||
_ sendReportCallback: @escaping UIAlertActionCallback,
|
||||
alwaysSendCallback: @escaping UIAlertActionCallback,
|
||||
dontSendCallback: @escaping UIAlertActionCallback) -> UIAlertController {
|
||||
|
||||
let alert = UIAlertController(
|
||||
title: NSLocalizedString("Oops! Firefox crashed", comment: "Title for prompt displayed to user after the app crashes"),
|
||||
message: NSLocalizedString("Send a crash report so Mozilla can fix the problem?", comment: "Message displayed in the crash dialog above the buttons used to select when sending reports"),
|
||||
preferredStyle: UIAlertControllerStyle.alert
|
||||
)
|
||||
|
||||
let sendReport = UIAlertAction(
|
||||
title: NSLocalizedString("Send Report", comment: "Used as a button label for crash dialog prompt"),
|
||||
style: UIAlertActionStyle.default,
|
||||
handler: sendReportCallback
|
||||
)
|
||||
|
||||
let alwaysSend = UIAlertAction(
|
||||
title: NSLocalizedString("Always Send", comment: "Used as a button label for crash dialog prompt"),
|
||||
style: UIAlertActionStyle.default,
|
||||
handler: alwaysSendCallback
|
||||
)
|
||||
|
||||
let dontSend = UIAlertAction(
|
||||
title: NSLocalizedString("Don’t Send", comment: "Used as a button label for crash dialog prompt"),
|
||||
style: UIAlertActionStyle.default,
|
||||
handler: dontSendCallback
|
||||
)
|
||||
|
||||
alert.addAction(sendReport)
|
||||
alert.addAction(alwaysSend)
|
||||
alert.addAction(dontSend)
|
||||
|
||||
return alert
|
||||
}
|
||||
|
||||
/**
|
||||
Builds the Alert view that asks the user if they wish to restore their tabs after a crash.
|
||||
|
||||
- parameter okayCallback: Okay option handler
|
||||
- parameter noCallback: No option handler
|
||||
|
||||
- returns: UIAlertController for asking the user to restore tabs after a crash
|
||||
*/
|
||||
class func restoreTabsAlert(okayCallback: @escaping UIAlertActionCallback, noCallback: @escaping UIAlertActionCallback) -> UIAlertController {
|
||||
let alert = UIAlertController(
|
||||
title: NSLocalizedString("Well, this is embarrassing.", comment: "Restore Tabs Prompt Title"),
|
||||
message: NSLocalizedString("Looks like Firefox crashed previously. Would you like to restore your tabs?", comment: "Restore Tabs Prompt Description"),
|
||||
preferredStyle: UIAlertControllerStyle.alert
|
||||
)
|
||||
|
||||
let noOption = UIAlertAction(
|
||||
title: NSLocalizedString("No", comment: "Restore Tabs Negative Action"),
|
||||
style: UIAlertActionStyle.cancel,
|
||||
handler: noCallback
|
||||
)
|
||||
|
||||
let okayOption = UIAlertAction(
|
||||
title: NSLocalizedString("Okay", comment: "Restore Tabs Affirmative Action"),
|
||||
style: UIAlertActionStyle.default,
|
||||
handler: okayCallback
|
||||
)
|
||||
|
||||
alert.addAction(okayOption)
|
||||
alert.addAction(noOption)
|
||||
return alert
|
||||
}
|
||||
|
||||
class func clearPrivateDataAlert(okayCallback: @escaping (UIAlertAction) -> Void) -> UIAlertController {
|
||||
let alert = UIAlertController(
|
||||
title: "",
|
||||
message: NSLocalizedString("This action will clear all of your private data. It cannot be undone.", tableName: "ClearPrivateDataConfirm", comment: "Description of the confirmation dialog shown when a user tries to clear their private data."),
|
||||
preferredStyle: UIAlertControllerStyle.alert
|
||||
)
|
||||
|
||||
let noOption = UIAlertAction(
|
||||
title: NSLocalizedString("Cancel", tableName: "ClearPrivateDataConfirm", comment: "The cancel button when confirming clear private data."),
|
||||
style: UIAlertActionStyle.cancel,
|
||||
handler: nil
|
||||
)
|
||||
|
||||
let okayOption = UIAlertAction(
|
||||
title: NSLocalizedString("OK", tableName: "ClearPrivateDataConfirm", comment: "The button that clears private data."),
|
||||
style: UIAlertActionStyle.destructive,
|
||||
handler: okayCallback
|
||||
)
|
||||
|
||||
alert.addAction(okayOption)
|
||||
alert.addAction(noOption)
|
||||
return alert
|
||||
}
|
||||
|
||||
/**
|
||||
Builds the Alert view that asks if the users wants to also delete history stored on their other devices.
|
||||
|
||||
- parameter okayCallback: Okay option handler.
|
||||
|
||||
- returns: UIAlertController for asking the user to restore tabs after a crash
|
||||
*/
|
||||
|
||||
class func clearSyncedHistoryAlert(okayCallback: @escaping (UIAlertAction) -> Void) -> UIAlertController {
|
||||
let alert = UIAlertController(
|
||||
title: "",
|
||||
message: NSLocalizedString("This action will clear all of your private data, including history from your synced devices.", tableName: "ClearHistoryConfirm", comment: "Description of the confirmation dialog shown when a user tries to clear history that's synced to another device."),
|
||||
preferredStyle: UIAlertControllerStyle.alert
|
||||
)
|
||||
|
||||
let noOption = UIAlertAction(
|
||||
title: NSLocalizedString("Cancel", tableName: "ClearHistoryConfirm", comment: "The cancel button when confirming clear history."),
|
||||
style: UIAlertActionStyle.cancel,
|
||||
handler: nil
|
||||
)
|
||||
|
||||
let okayOption = UIAlertAction(
|
||||
title: NSLocalizedString("OK", tableName: "ClearHistoryConfirm", comment: "The confirmation button that clears history even when Sync is connected."),
|
||||
style: UIAlertActionStyle.destructive,
|
||||
handler: okayCallback
|
||||
)
|
||||
|
||||
alert.addAction(okayOption)
|
||||
alert.addAction(noOption)
|
||||
return alert
|
||||
}
|
||||
|
||||
/**
|
||||
Creates an alert view to warn the user that their logins will either be completely deleted in the
|
||||
case of local-only logins or deleted across synced devices in synced account logins.
|
||||
|
||||
- parameter deleteCallback: Block to run when delete is tapped.
|
||||
- parameter hasSyncedLogins: Boolean indicating the user has logins that have been synced.
|
||||
|
||||
- returns: UIAlertController instance
|
||||
*/
|
||||
class func deleteLoginAlertWithDeleteCallback(
|
||||
_ deleteCallback: @escaping UIAlertActionCallback,
|
||||
hasSyncedLogins: Bool) -> UIAlertController {
|
||||
|
||||
let areYouSureTitle = NSLocalizedString("Are you sure?",
|
||||
tableName: "LoginManager",
|
||||
comment: "Prompt title when deleting logins")
|
||||
let deleteLocalMessage = NSLocalizedString("Logins will be permanently removed.",
|
||||
tableName: "LoginManager",
|
||||
comment: "Prompt message warning the user that deleting non-synced logins will permanently remove them")
|
||||
let deleteSyncedDevicesMessage = NSLocalizedString("Logins will be removed from all connected devices.",
|
||||
tableName: "LoginManager",
|
||||
comment: "Prompt message warning the user that deleted logins will remove logins from all connected devices")
|
||||
let cancelActionTitle = NSLocalizedString("Cancel",
|
||||
tableName: "LoginManager",
|
||||
comment: "Prompt option for cancelling out of deletion")
|
||||
let deleteActionTitle = NSLocalizedString("Delete",
|
||||
tableName: "LoginManager",
|
||||
comment: "Label for the button used to delete the current login.")
|
||||
|
||||
let deleteAlert: UIAlertController
|
||||
if hasSyncedLogins {
|
||||
deleteAlert = UIAlertController(title: areYouSureTitle, message: deleteSyncedDevicesMessage, preferredStyle: .alert)
|
||||
} else {
|
||||
deleteAlert = UIAlertController(title: areYouSureTitle, message: deleteLocalMessage, preferredStyle: .alert)
|
||||
}
|
||||
|
||||
let cancelAction = UIAlertAction(title: cancelActionTitle, style: .cancel, handler: nil)
|
||||
let deleteAction = UIAlertAction(title: deleteActionTitle, style: .destructive, handler: deleteCallback)
|
||||
|
||||
deleteAlert.addAction(cancelAction)
|
||||
deleteAlert.addAction(deleteAction)
|
||||
|
||||
return deleteAlert
|
||||
}
|
||||
}
|
||||
87
mobile/ios/Client/Extensions/UIImageViewExtensions.swift
Normal file
87
mobile/ios/Client/Extensions/UIImageViewExtensions.swift
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import UIKit
|
||||
import Storage
|
||||
import SDWebImage
|
||||
import Shared
|
||||
|
||||
public extension UIImageView {
|
||||
|
||||
public func setIcon(_ icon: Favicon?, forURL url: URL?, completed completionBlock: ((UIColor, URL?) -> Void)? = nil ) {
|
||||
if let url = url, let defaultIcon = FaviconFetcher.getDefaultIconForURL(url: url) {
|
||||
self.image = UIImage(contentsOfFile: defaultIcon.url)
|
||||
self.backgroundColor = defaultIcon.color
|
||||
completionBlock?(defaultIcon.color, url)
|
||||
} else {
|
||||
let imageURL = URL(string: icon?.url ?? "")
|
||||
let defaults = defaultFavicon(url)
|
||||
self.sd_setImage(with: imageURL, placeholderImage: defaults.image, options: []) {(img, err, _, _) in
|
||||
guard let image = img, let dUrl = url, err == nil else {
|
||||
self.backgroundColor = defaults.color
|
||||
completionBlock?(defaults.color, url)
|
||||
return
|
||||
}
|
||||
self.color(forImage: image, andURL: dUrl, completed: completionBlock)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Fetch a background color for a specfic favicon UIImage. It uses the URL to store the UIColor in memory for subsequent requests.
|
||||
*/
|
||||
private func color(forImage image: UIImage, andURL url: URL, completed completionBlock: ((UIColor, URL?) -> Void)? = nil) {
|
||||
guard let domain = url.baseDomain else {
|
||||
self.backgroundColor = .gray
|
||||
completionBlock?(UIColor.gray, url)
|
||||
return
|
||||
}
|
||||
|
||||
if let color = FaviconFetcher.colors[domain] {
|
||||
self.backgroundColor = color
|
||||
completionBlock?(color, url)
|
||||
} else {
|
||||
image.getColors(scaleDownSize: CGSize(width: 25, height: 25)) {colors in
|
||||
let isSame = [colors.primary, colors.secondary, colors.detail].every { $0 == colors.primary }
|
||||
if isSame {
|
||||
completionBlock?(UIColor.white, url)
|
||||
FaviconFetcher.colors[domain] = UIColor.white
|
||||
} else {
|
||||
completionBlock?(colors.background, url)
|
||||
FaviconFetcher.colors[domain] = colors.background
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func setFavicon(forSite site: Site, onCompletion completionBlock: ((UIColor, URL?) -> Void)? = nil ) {
|
||||
self.setIcon(site.icon, forURL: site.tileURL, completed: completionBlock)
|
||||
}
|
||||
|
||||
private func defaultFavicon(_ url: URL?) -> (image: UIImage, color: UIColor) {
|
||||
if let url = url {
|
||||
return (FaviconFetcher.getDefaultFavicon(url), FaviconFetcher.getDefaultColor(url))
|
||||
} else {
|
||||
return (FaviconFetcher.defaultFavicon, .white)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
open class ImageOperation: NSObject, SDWebImageOperation {
|
||||
open var cacheOperation: Operation?
|
||||
|
||||
var cancelled: Bool {
|
||||
if let cacheOperation = cacheOperation {
|
||||
return cacheOperation.isCancelled
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@objc open func cancel() {
|
||||
if let cacheOperation = cacheOperation {
|
||||
cacheOperation.cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
63
mobile/ios/Client/Extensions/UIPasteboardExtensions.swift
Normal file
63
mobile/ios/Client/Extensions/UIPasteboardExtensions.swift
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import Deferred
|
||||
import Foundation
|
||||
import MobileCoreServices
|
||||
import Shared
|
||||
import UIKit
|
||||
|
||||
extension UIPasteboard {
|
||||
func addImageWithData(_ data: Data, forURL url: URL) {
|
||||
let isGIF = UIImage.dataIsGIF(data)
|
||||
|
||||
// Setting pasteboard.items allows us to set multiple representations for the same item.
|
||||
items = [[
|
||||
kUTTypeURL as String: url,
|
||||
imageTypeKey(isGIF): data
|
||||
]]
|
||||
}
|
||||
|
||||
fileprivate func imageTypeKey(_ isGIF: Bool) -> String {
|
||||
return (isGIF ? kUTTypeGIF : kUTTypePNG) as String
|
||||
}
|
||||
|
||||
private var syncURL: URL? {
|
||||
if let string = UIPasteboard.general.string,
|
||||
let url = URL(string: string), url.isWebPage() {
|
||||
return url
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
/// Preferred method to get strings out of the clipboard.
|
||||
/// When iCloud pasteboards are enabled, the usually fast, synchronous calls
|
||||
/// become slow and synchronous causing very slow start up times.
|
||||
func asyncString() -> Deferred<Maybe<String?>> {
|
||||
return fetchAsync() {
|
||||
return UIPasteboard.general.string
|
||||
}
|
||||
}
|
||||
|
||||
/// Preferred method to get URLs out of the clipboard.
|
||||
/// We use Deferred<Maybe<T?>> to fit in to the rest of the Deferred<Maybe> tools
|
||||
/// we already use; but use optionals instead of errorTypes, because not having a URL
|
||||
/// on the clipboard isn't an error.
|
||||
func asyncURL() -> Deferred<Maybe<URL?>> {
|
||||
return fetchAsync() {
|
||||
return self.syncURL
|
||||
}
|
||||
}
|
||||
|
||||
// Converts the potentially long running synchronous operation into an asynchronous one.
|
||||
private func fetchAsync<T>(getter: @escaping () -> T) -> Deferred<Maybe<T>> {
|
||||
let deferred = Deferred<Maybe<T>>()
|
||||
DispatchQueue.global().async {
|
||||
let value = getter()
|
||||
deferred.fill(Maybe(success: value))
|
||||
}
|
||||
return deferred
|
||||
}
|
||||
}
|
||||
94
mobile/ios/Client/Extensions/UIViewExtensions.swift
Normal file
94
mobile/ios/Client/Extensions/UIViewExtensions.swift
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
import Foundation
|
||||
|
||||
extension UIView {
|
||||
/**
|
||||
* Takes a screenshot of the view with the given size.
|
||||
*/
|
||||
func screenshot(_ size: CGSize, offset: CGPoint? = nil, quality: CGFloat = 1) -> UIImage? {
|
||||
assert(0...1 ~= quality)
|
||||
|
||||
let offset = offset ?? CGPoint(x: 0, y: 0)
|
||||
|
||||
UIGraphicsBeginImageContextWithOptions(size, false, UIScreen.main.scale * quality)
|
||||
drawHierarchy(in: CGRect(origin: offset, size: frame.size), afterScreenUpdates: false)
|
||||
let image = UIGraphicsGetImageFromCurrentImageContext()
|
||||
UIGraphicsEndImageContext()
|
||||
|
||||
return image
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a screenshot of the view with the given aspect ratio.
|
||||
* An aspect ratio of 0 means capture the entire view.
|
||||
*/
|
||||
func screenshot(_ aspectRatio: CGFloat = 0, offset: CGPoint? = nil, quality: CGFloat = 1) -> UIImage? {
|
||||
assert(aspectRatio >= 0)
|
||||
|
||||
var size: CGSize
|
||||
if aspectRatio > 0 {
|
||||
size = CGSize()
|
||||
let viewAspectRatio = frame.width / frame.height
|
||||
if viewAspectRatio > aspectRatio {
|
||||
size.height = frame.height
|
||||
size.width = size.height * aspectRatio
|
||||
} else {
|
||||
size.width = frame.width
|
||||
size.height = size.width / aspectRatio
|
||||
}
|
||||
} else {
|
||||
size = frame.size
|
||||
}
|
||||
|
||||
return screenshot(size, offset: offset, quality: quality)
|
||||
}
|
||||
|
||||
/*
|
||||
* Performs a deep copy of the view. Does not copy constraints.
|
||||
*/
|
||||
func clone() -> UIView {
|
||||
let data = NSKeyedArchiver.archivedData(withRootObject: self)
|
||||
return NSKeyedUnarchiver.unarchiveObject(with: data) as! UIView
|
||||
}
|
||||
|
||||
/**
|
||||
* rounds the requested corners of a view with the provided radius
|
||||
*/
|
||||
func addRoundedCorners(_ cornersToRound: UIRectCorner, cornerRadius: CGSize, color: UIColor) {
|
||||
let rect = bounds
|
||||
let maskPath = UIBezierPath(roundedRect: rect, byRoundingCorners: cornersToRound, cornerRadii: cornerRadius)
|
||||
|
||||
// Create the shape layer and set its path
|
||||
let maskLayer = CAShapeLayer()
|
||||
maskLayer.frame = rect
|
||||
maskLayer.path = maskPath.cgPath
|
||||
|
||||
let roundedLayer = CALayer()
|
||||
roundedLayer.backgroundColor = color.cgColor
|
||||
roundedLayer.frame = rect
|
||||
roundedLayer.mask = maskLayer
|
||||
|
||||
layer.insertSublayer(roundedLayer, at: 0)
|
||||
backgroundColor = UIColor.clear
|
||||
}
|
||||
|
||||
/**
|
||||
This allows us to find the view in a current view hierarchy that is currently the first responder
|
||||
*/
|
||||
static func findSubViewWithFirstResponder(_ view: UIView) -> UIView? {
|
||||
let subviews = view.subviews
|
||||
if subviews.count == 0 {
|
||||
return nil
|
||||
}
|
||||
for subview: UIView in subviews {
|
||||
if subview.isFirstResponder {
|
||||
return subview
|
||||
}
|
||||
return findSubViewWithFirstResponder(subview)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue