diff --git a/ios/src_library/Temp/Extensions+Init/CAGradientLayerType+Init.swift b/ios/src_library/Temp/Extensions+Init/CAGradientLayerType+Init.swift new file mode 100644 index 00000000..56b06b00 --- /dev/null +++ b/ios/src_library/Temp/Extensions+Init/CAGradientLayerType+Init.swift @@ -0,0 +1,27 @@ +// +// CAGradientLayerType+Init.swift +// react-native-ios-navigator +// +// Created by Dominic Go on 4/12/21. +// + +import Foundation + +public extension CAGradientLayerType { + init?(string: String){ + switch string { + case "axial" : self = .axial; + case "radial": self = .radial; + + case "conic" : + if #available(iOS 12.0, *) { + self = .conic; + + } else { + return nil; + }; + + default: return nil; + } + }; +}; diff --git a/ios/src_library/Temp/Extensions+Init/UIImage+Init.swift b/ios/src_library/Temp/Extensions+Init/UIImage+Init.swift new file mode 100644 index 00000000..8ea948c4 --- /dev/null +++ b/ios/src_library/Temp/Extensions+Init/UIImage+Init.swift @@ -0,0 +1,56 @@ +// +// UIImage+Init.swift +// react-native-ios-navigator +// +// Created by Dominic Go on 10/2/21. +// + +import Foundation +import UIKit + +public extension UIImage.RenderingMode { + init?(string: String){ + switch string { + case "automatic" : self = .automatic; + case "alwaysOriginal": self = .alwaysOriginal; + case "alwaysTemplate": self = .alwaysTemplate; + + default: return nil; + }; + }; +}; + +@available(iOS 13.0, *) +public extension UIImage.SymbolWeight { + init?(string: String){ + switch string { + case "unspecified": self = .unspecified; + case "ultraLight" : self = .ultraLight; + case "thin" : self = .thin; + case "light" : self = .light; + case "regular" : self = .regular; + case "medium" : self = .medium; + case "semibold" : self = .semibold; + case "bold" : self = .bold; + case "heavy" : self = .heavy; + case "black" : self = .black; + + default: return nil; + }; + }; +}; + +@available(iOS 13.0, *) +public extension UIImage.SymbolScale { + init?(string: String){ + switch string { + case "default" : self = .`default`; + case "unspecified" : self = .unspecified; + case "small" : self = .small; + case "medium" : self = .medium; + case "large" : self = .large; + + default: return nil; + }; + }; +}; diff --git a/ios/src_library/Temp/Extensions/Encodable+Helpers.swift b/ios/src_library/Temp/Extensions/Encodable+Helpers.swift new file mode 100644 index 00000000..59be3f78 --- /dev/null +++ b/ios/src_library/Temp/Extensions/Encodable+Helpers.swift @@ -0,0 +1,39 @@ +// +// Encodable+Helpers.swift +// nativeUIModulesTest +// +// Created by Dominic Go on 7/6/20. +// + +import Foundation + +public struct JSON { + static let encoder = JSONEncoder(); +}; + +public extension Encodable { + subscript(key: String) -> Any? { + return dictionary[key]; + }; + + var dictionary: [String: Any] { + return + (try? JSONSerialization.jsonObject(with: JSON.encoder.encode(self))) + as? [String: Any] ?? [:]; + }; + + func jsonData() throws -> Data { + let encoder = JSON.encoder; + + encoder.outputFormatting = .prettyPrinted; + + if #available(iOS 10.0, *) { + encoder.dateEncodingStrategy = .iso8601; + + } else { + encoder.dateEncodingStrategy = .millisecondsSince1970; + }; + + return try encoder.encode(self); + }; +}; diff --git a/ios/src_library/Temp/Extensions/UIColor+Helpers.swift b/ios/src_library/Temp/Extensions/UIColor+Helpers.swift new file mode 100644 index 00000000..e473a0ee --- /dev/null +++ b/ios/src_library/Temp/Extensions/UIColor+Helpers.swift @@ -0,0 +1,446 @@ +// +// UIColor+Helpers.swift +// IosContextMenuExample +// +// Created by Dominic Go on 11/12/20. +// Copyright © 2020 Facebook. All rights reserved. +// +import UIKit; + + +fileprivate class UIColorHelpers { + struct RGBColor { + var r: CGFloat; + var g: CGFloat; + var b: CGFloat; + + init(r: Int, g: Int, b: Int) { + self.r = CGFloat(r) / 255; + self.g = CGFloat(g) / 255; + self.b = CGFloat(b) / 255; + } + }; + + // css colors strings to UIColor + static let cssColorsToRGB: [String: RGBColor] = [ + // colors red --------------------------------- + "lightsalmon": RGBColor(r: 255, g: 160, b: 122), + "salmon" : RGBColor(r: 250, g: 128, b: 114), + "darksalmon" : RGBColor(r: 233, g: 150, b: 122), + "lightcoral" : RGBColor(r: 240, g: 128, b: 128), + "indianred" : RGBColor(r: 205, g: 92 , b: 92 ), + "crimson" : RGBColor(r: 220, g: 20 , b: 60 ), + "firebrick" : RGBColor(r: 178, g: 34 , b: 34 ), + "red" : RGBColor(r: 255, g: 0 , b: 0 ), + "darkred" : RGBColor(r: 139, g: 0 , b: 0 ), + // colors orange ---------------------------- + "coral" : RGBColor(r: 255, g: 127, b: 80), + "tomato" : RGBColor(r: 255, g: 99 , b: 71), + "orangered" : RGBColor(r: 255, g: 69 , b: 0 ), + "gold" : RGBColor(r: 255, g: 215, b: 0 ), + "orange" : RGBColor(r: 255, g: 165, b: 0 ), + "darkorange": RGBColor(r: 255, g: 140, b: 0 ), + // colors green ---------------------------------------- + "lightyellow" : RGBColor(r: 255, g: 255, b: 224), + "lemonchiffon" : RGBColor(r: 255, g: 250, b: 205), + "lightgoldenrodyellow": RGBColor(r: 250, g: 250, b: 210), + "papayawhip" : RGBColor(r: 255, g: 239, b: 213), + "moccasin" : RGBColor(r: 255, g: 228, b: 181), + "peachpuff" : RGBColor(r: 255, g: 218, b: 185), + "palegoldenrod" : RGBColor(r: 238, g: 232, b: 170), + "khaki" : RGBColor(r: 240, g: 230, b: 140), + "darkkhaki" : RGBColor(r: 189, g: 183, b: 107), + "yellow" : RGBColor(r: 255, g: 255, b: 0 ), + // colors green ------------------------------------- + "lawngreen" : RGBColor(r: 124, g: 252, b: 0 ), + "chartreuse" : RGBColor(r: 127, g: 255, b: 0 ), + "limegreen" : RGBColor(r: 50 , g: 205, b: 50 ), + "lime" : RGBColor(r: 0 , g: 255, b: 0 ), + "forestgreen" : RGBColor(r: 34 , g: 139, b: 34 ), + "green" : RGBColor(r: 0 , g: 128, b: 0 ), + "darkgreen" : RGBColor(r: 0 , g: 100, b: 0 ), + "greenyellow" : RGBColor(r: 173, g: 255, b: 47 ), + "yellowgreen" : RGBColor(r: 154, g: 205, b: 50 ), + "springgreen" : RGBColor(r: 0 , g: 255, b: 127), + "mediumspringgreen": RGBColor(r: 0 , g: 250, b: 154), + "lightgreen" : RGBColor(r: 144, g: 238, b: 144), + "palegreen" : RGBColor(r: 152, g: 251, b: 152), + "darkseagreen" : RGBColor(r: 143, g: 188, b: 143), + "mediumseagreen" : RGBColor(r: 60 , g: 179, b: 113), + "seagreen" : RGBColor(r: 46 , g: 139, b: 87 ), + "olive" : RGBColor(r: 128, g: 128, b: 0 ), + "darkolivegreen" : RGBColor(r: 85 , g: 107, b: 47 ), + "olivedrab" : RGBColor(r: 107, g: 142, b: 35 ), + // colors cyan ------------------------------------- + "lightcyan" : RGBColor(r: 224, g: 255, b: 255), + "cyan" : RGBColor(r: 0 , g: 255, b: 255), + "aqua" : RGBColor(r: 0 , g: 255, b: 255), + "aquamarine" : RGBColor(r: 127, g: 255, b: 212), + "mediumaquamarine": RGBColor(r: 102, g: 205, b: 170), + "paleturquoise" : RGBColor(r: 175, g: 238, b: 238), + "turquoise" : RGBColor(r: 64 , g: 224, b: 208), + "mediumturquoise" : RGBColor(r: 72 , g: 209, b: 204), + "darkturquoise" : RGBColor(r: 0 , g: 206, b: 209), + "lightseagreen" : RGBColor(r: 32 , g: 178, b: 170), + "cadetblue" : RGBColor(r: 95 , g: 158, b: 160), + "darkcyan" : RGBColor(r: 0 , g: 139, b: 139), + "teal" : RGBColor(r: 0 , g: 128, b: 128), + // colors blue ------------------------------------ + "powderblue" : RGBColor(r: 176, g: 224, b: 230), + "lightblue" : RGBColor(r: 173, g: 216, b: 230), + "lightskyblue" : RGBColor(r: 135, g: 206, b: 250), + "skyblue" : RGBColor(r: 135, g: 206, b: 235), + "deepskyblue" : RGBColor(r: 0 , g: 191, b: 255), + "lightsteelblue" : RGBColor(r: 176, g: 196, b: 222), + "dodgerblue" : RGBColor(r: 30 , g: 144, b: 255), + "cornflowerblue" : RGBColor(r: 100, g: 149, b: 237), + "steelblue" : RGBColor(r: 70 , g: 130, b: 180), + "royalblue" : RGBColor(r: 65 , g: 105, b: 225), + "blue" : RGBColor(r: 0 , g: 0 , b: 255), + "mediumblue" : RGBColor(r: 0 , g: 0 , b: 205), + "darkblue" : RGBColor(r: 0 , g: 0 , b: 139), + "navy" : RGBColor(r: 0 , g: 0 , b: 128), + "midnightblue" : RGBColor(r: 25 , g: 25 , b: 112), + "mediumslateblue": RGBColor(r: 123, g: 104, b: 238), + "slateblue" : RGBColor(r: 106, g: 90 , b: 205), + "darkslateblue" : RGBColor(r: 72 , g: 61 , b: 139), + // colors purple ------------------------------- + "lavender" : RGBColor(r: 230, g: 230, b: 250), + "thistle" : RGBColor(r: 216, g: 191, b: 216), + "plum" : RGBColor(r: 221, g: 160, b: 221), + "violet" : RGBColor(r: 238, g: 130, b: 238), + "orchid" : RGBColor(r: 218, g: 112, b: 214), + "fuchsia" : RGBColor(r: 255, g: 0 , b: 255), + "magenta" : RGBColor(r: 255, g: 0 , b: 255), + "mediumorchid": RGBColor(r: 186, g: 85 , b: 211), + "mediumpurple": RGBColor(r: 147, g: 112, b: 219), + "blueviolet" : RGBColor(r: 138, g: 43 , b: 226), + "darkviolet" : RGBColor(r: 148, g: 0 , b: 211), + "darkorchid" : RGBColor(r: 153, g: 50 , b: 204), + "darkmagenta" : RGBColor(r: 139, g: 0 , b: 139), + "purple" : RGBColor(r: 128, g: 0 , b: 128), + "indigo" : RGBColor(r: 75 , g: 0 , b: 130), + // colors pink ------------------------------------ + "pink" : RGBColor(r: 255, g: 192, b: 203), + "lightpink" : RGBColor(r: 255, g: 182, b: 193), + "hotpink" : RGBColor(r: 255, g: 105, b: 180), + "deeppink" : RGBColor(r: 255, g: 20 , b: 147), + "palevioletred" : RGBColor(r: 219, g: 112, b: 147), + "mediumvioletred": RGBColor(r: 199, g: 21 , b: 133), + // colors white --------------------------------- + "white" : RGBColor(r: 255, g: 255, b: 255), + "snow" : RGBColor(r: 255, g: 250, b: 250), + "honeydew" : RGBColor(r: 240, g: 255, b: 240), + "mintcream" : RGBColor(r: 245, g: 255, b: 250), + "azure" : RGBColor(r: 240, g: 255, b: 255), + "aliceblue" : RGBColor(r: 240, g: 248, b: 255), + "ghostwhite" : RGBColor(r: 248, g: 248, b: 255), + "whitesmoke" : RGBColor(r: 245, g: 245, b: 245), + "seashell" : RGBColor(r: 255, g: 245, b: 238), + "beige" : RGBColor(r: 245, g: 245, b: 220), + "oldlace" : RGBColor(r: 253, g: 245, b: 230), + "floralwhite" : RGBColor(r: 255, g: 250, b: 240), + "ivory" : RGBColor(r: 255, g: 255, b: 240), + "antiquewhite" : RGBColor(r: 250, g: 235, b: 215), + "linen" : RGBColor(r: 250, g: 240, b: 230), + "lavenderblush": RGBColor(r: 255, g: 240, b: 245), + "mistyrose" : RGBColor(r: 255, g: 228, b: 225), + // colors gray ----------------------------------- + "gainsboro" : RGBColor(r: 220, g: 220, b: 220), + "lightgray" : RGBColor(r: 211, g: 211, b: 211), + "silver" : RGBColor(r: 192, g: 192, b: 192), + "darkgray" : RGBColor(r: 169, g: 169, b: 169), + "gray" : RGBColor(r: 128, g: 128, b: 128), + "dimgray" : RGBColor(r: 105, g: 105, b: 105), + "lightslategray": RGBColor(r: 119, g: 136, b: 153), + "slategray" : RGBColor(r: 112, g: 128, b: 144), + "darkslategray" : RGBColor(r: 47 , g: 79 , b: 79 ), + "black" : RGBColor(r: 0 , g: 0 , b: 0 ), + // colors brown ---------------------------------- + "cornsilk" : RGBColor(r: 255, g: 248, b: 220), + "blanchedalmond": RGBColor(r: 255, g: 235, b: 205), + "bisque" : RGBColor(r: 255, g: 228, b: 196), + "navajowhite" : RGBColor(r: 255, g: 222, b: 173), + "wheat" : RGBColor(r: 245, g: 222, b: 179), + "burlywood" : RGBColor(r: 222, g: 184, b: 135), + "tan" : RGBColor(r: 210, g: 180, b: 140), + "rosybrown" : RGBColor(r: 188, g: 143, b: 143), + "sandybrown" : RGBColor(r: 244, g: 164, b: 96 ), + "goldenrod" : RGBColor(r: 218, g: 165, b: 32 ), + "peru" : RGBColor(r: 205, g: 133, b: 63 ), + "chocolate" : RGBColor(r: 210, g: 105, b: 30 ), + "saddlebrown" : RGBColor(r: 139, g: 69 , b: 19 ), + "sienna" : RGBColor(r: 160, g: 82 , b: 45 ), + "brown" : RGBColor(r: 165, g: 42 , b: 42 ), + "maroon" : RGBColor(r: 128, g: 0 , b: 0 ) + ]; + + static func normalizeHexString(_ hex: String?) -> String { + guard var hexString = hex else { + return "00000000"; + }; + + if hexString.hasPrefix("#") { + hexString = String(hexString.dropFirst()); + }; + + if hexString.count == 3 || hexString.count == 4 { + hexString = hexString.map { "\($0)\($0)" }.joined(); + }; + + let hasAlpha = hexString.count > 7; + if !hasAlpha { + hexString += "ff"; + }; + + return hexString; + }; +}; + +public extension UIColor { + + var rgba: (r: CGFloat, g: CGFloat, b: CGFloat, a: CGFloat) { + var red : CGFloat = 0; + var green: CGFloat = 0; + var blue : CGFloat = 0; + var alpha: CGFloat = 0; + + getRed(&red, green: &green, blue: &blue, alpha: &alpha); + return (red, green, blue, alpha); + }; + + @available(iOS 13.0, *) + static func elementColorFromString(_ string: String) -> UIColor? { + switch string { + // Label Colors + case "label": return .label; + case "secondaryLabel": return .secondaryLabel; + case "tertiaryLabel": return .tertiaryLabel; + case "quaternaryLabel": return .quaternaryLabel; + + // Fill Colors + case "systemFill": return .systemFill; + case "secondarySystemFill": return .secondarySystemFill; + case "tertiarySystemFill": return .tertiarySystemFill; + case "quaternarySystemFill": return .quaternarySystemFill; + + // Text Colors + case "placeholderText": return .placeholderText; + + // Standard Content Background Colors + case "systemBackground": return .systemBackground; + case "secondarySystemBackground": return .secondarySystemBackground; + case "tertiarySystemBackground": return .tertiarySystemBackground; + + // Grouped Content Background Colors + case "systemGroupedBackground": return .systemGroupedBackground; + case "secondarySystemGroupedBackground": return .secondarySystemGroupedBackground; + case "tertiarySystemGroupedBackground": return .tertiarySystemGroupedBackground; + + // Separator Colors + case "separator": return .separator; + case "opaqueSeparator": return .opaqueSeparator; + + // Link Color + case "link": return .link; + + // Non-adaptable Colors + case "darkText": return .darkText; + case "lightText": return .lightText; + + default: return nil; + }; + }; + + static func systemColorFromString(_ string: String) -> UIColor? { + switch string { + // Adaptable Colors + case "systemBlue" : return .systemBlue; + case "systemGreen" : return .systemGreen; + case "systemOrange": return .systemOrange; + case "systemPink" : return .systemPink; + case "systemPurple": return .systemPurple; + case "systemRed" : return .systemRed; + case "systemTeal" : return .systemTeal; + case "systemYellow": return .systemYellow; + + default: break; + }; + + if #available(iOS 13.0, *) { + switch string { + case "systemIndigo" : return .systemIndigo; + + //Adaptable Gray Colors + case "systemGray" : return .systemGray; + case "systemGray2": return .systemGray2; + case "systemGray3": return .systemGray3; + case "systemGray4": return .systemGray4; + case "systemGray5": return .systemGray5; + case "systemGray6": return .systemGray6; + + default: break; + }; + }; + + return nil; + }; + + /// Parse "react native" color to `UIColor` + /// Swift impl. `RCTConvert` color + static func parseColor(value: Any) -> UIColor? { + if let string = value as? String { + if #available(iOS 13.0, *), + let color = Self.elementColorFromString(string) { + + // a: iOS 13+ ui enum colors + return color; + + } else if let color = Self.systemColorFromString(string) { + // b: iOS system enum colors + return color; + + } else { + // c: react-native color string + return UIColor(cssColor: string); + }; + + } else if let dict = value as? NSDictionary { + // d: react-native DynamicColor object + return UIColor(dynamicDict: dict); + }; + + return nil; + }; + + /// create color from css color code string + convenience init?(cssColorCode: String) { + guard let color = UIColorHelpers.cssColorsToRGB[cssColorCode.lowercased()] + else { return nil }; + + self.init(red: color.r, green: color.g, blue: color.b, alpha: 1); + }; + + /// create color from hex color string + convenience init?(hexString: String) { + guard hexString.hasPrefix("#") else { return nil }; + let hexColor: String = UIColorHelpers.normalizeHexString(hexString); + + // invalid hex string + guard hexColor.count == 8 else { return nil }; + + var hexNumber: UInt64 = 0; + let scanner = Scanner(string: hexColor); + + // failed to convert hex string + guard scanner.scanHexInt64(&hexNumber) else { return nil }; + + self.init( + red : CGFloat((hexNumber & 0xff000000) >> 24) / 255, + green: CGFloat((hexNumber & 0x00ff0000) >> 16) / 255, + blue : CGFloat((hexNumber & 0x0000ff00) >> 8 ) / 255, + alpha: CGFloat( hexNumber & 0x000000ff) / 255 + ); + }; + + /// create color from rgb/rgba string + convenience init?(rgbString: String){ + // create mutable copy... + var rgbString = rgbString; + + // check if rgba() string + let hasAlpha = rgbString.hasPrefix("rgba"); + + // remove "rgb(" or "rgba" prefix + rgbString = rgbString.replacingOccurrences( + of: hasAlpha ? "rgba(" : "rgb(", + with: "", + options: [.caseInsensitive] + ); + + // remove ")" suffix + rgbString = rgbString.replacingOccurrences( + of: ")", with: "", options: [.backwards] + ); + + // split up the rgb values seperated by "," + let split = rgbString.components(separatedBy: ","); + + // convert to array of float + let colors = split.compactMap { + NumberFormatter().number(from: $0) as? CGFloat; + }; + + if(colors.count == 3) { + // create UIColor from rgb(...) string + self.init( + red : colors[0] / 255, + green: colors[1] / 255, + blue : colors[2] / 255, + alpha: 1 + ); + + } else if(colors.count == 4) { + // create UIColor from rgba(...) string + self.init( + red : colors[0] / 255, + green: colors[1] / 255, + blue : colors[2] / 255, + alpha: colors[3] + ); + + } else { + // invalid rgb color string + // color array is < 3 or > 4 + return nil; + }; + }; + + /// create color from rgb/rgba/hex/csscolor strings + convenience init?(cssColor: String){ + // remove whitespace characters + let colorString = cssColor.trimmingCharacters(in: .whitespacesAndNewlines); + + if colorString.hasPrefix("#"){ + self.init(hexString: colorString); + return; + + } else if colorString.hasPrefix("rgb") { + self.init(rgbString: colorString); + + } else if let color = UIColorHelpers.cssColorsToRGB[colorString.lowercased()] { + self.init(red: color.r, green: color.g, blue: color.b, alpha: 1); + return; + + } else { + return nil; + }; + }; + + /// create color from `DynamicColorIOS` dictionary + convenience init?(dynamicDict: NSDictionary) { + guard let dict = dynamicDict["dynamic"] as? NSDictionary, + let stringDark = dict["dark" ] as? String, + let stringLight = dict["light"] as? String + else { return nil }; + + if #available(iOS 13.0, *), + let colorDark = UIColor(cssColor: stringDark ), + let colorLight = UIColor(cssColor: stringLight) { + + self.init(dynamicProvider: { traitCollection in + switch traitCollection.userInterfaceStyle { + case .dark : return colorDark; + case .light: return colorLight; + + case .unspecified: fallthrough; + @unknown default : return .clear; + }; + }); + + } else { + self.init(cssColor: stringLight); + }; + }; + +}; diff --git a/ios/src_library/Temp/Extensions/UIViewController+Helpers.swift b/ios/src_library/Temp/Extensions/UIViewController+Helpers.swift new file mode 100644 index 00000000..a288be62 --- /dev/null +++ b/ios/src_library/Temp/Extensions/UIViewController+Helpers.swift @@ -0,0 +1,24 @@ +// +// UIViewController+Helpers.swift +// react-native-ios-utilities +// +// Created by Dominic Go on 8/26/22. +// + +import Foundation + +public extension UIViewController { + func attachChildVC(_ child: UIViewController) { + self.addChild(child); + self.view.addSubview(child.view); + child.didMove(toParent: self); + }; + + func detachFromParentVC() { + guard self.parent != nil else { return }; + + self.willMove(toParent: nil); + self.view.removeFromSuperview(); + self.removeFromParent(); + }; +}; diff --git a/ios/src_library/Temp/IosUtilities-Bridging-Header.h b/ios/src_library/Temp/IosUtilities-Bridging-Header.h new file mode 100644 index 00000000..98c35e95 --- /dev/null +++ b/ios/src_library/Temp/IosUtilities-Bridging-Header.h @@ -0,0 +1,18 @@ +#if DEBUG +#import +#endif + +#import "RNILog/_RNILog.h" + +#import +#import + +#import +#import + +#import +#import + +#import +#import +#import diff --git a/ios/src_library/Temp/Protocols/RNIJSComponentWillUnmountNotifiable.swift b/ios/src_library/Temp/Protocols/RNIJSComponentWillUnmountNotifiable.swift new file mode 100644 index 00000000..77b0eb9c --- /dev/null +++ b/ios/src_library/Temp/Protocols/RNIJSComponentWillUnmountNotifiable.swift @@ -0,0 +1,18 @@ +// +// RNIJSComponentWillUnmountNotifiable.swift +// react-native-ios-context-menu +// +// Created by Dominic Go on 9/25/22. +// + +import Foundation + + +/// +/// When a class implements this protocol, it means that it receives a notification from JS-side whenever +/// the component's `componentWillUnmount` lifecycle is triggered. +public protocol RNIJSComponentWillUnmountNotifiable { + + func notifyOnJSComponentWillUnmount(); + +}; diff --git a/ios/src_library/Temp/RNICleanup/RNICleanable.swift b/ios/src_library/Temp/RNICleanup/RNICleanable.swift new file mode 100644 index 00000000..fe257936 --- /dev/null +++ b/ios/src_library/Temp/RNICleanup/RNICleanable.swift @@ -0,0 +1,17 @@ +// +// RNICleanable.swift +// react-native-ios-popover +// +// Created by Dominic Go on 3/13/22. +// + +import Foundation + +/// +/// When a class implements this protocol, it means that the class has "clean-up" related code. +/// This is usually for `UIView` subclasses, and a +public protocol RNICleanable: AnyObject { + + func cleanup(); + +}; diff --git a/ios/src_library/Temp/RNICleanup/RNICleanupMode.swift b/ios/src_library/Temp/RNICleanup/RNICleanupMode.swift new file mode 100644 index 00000000..1b0cd9dc --- /dev/null +++ b/ios/src_library/Temp/RNICleanup/RNICleanupMode.swift @@ -0,0 +1,25 @@ +// +// RNICleanupMode.swift +// react-native-ios-context-menu +// +// Created by Dominic Go on 9/20/22. +// + +import Foundation + + +/// If a class conforms to `RNICleanable`, this enum determines how the cleanup routine is triggered. +public enum RNICleanupMode: String { + + case automatic; + + /// Trigger cleanup via view controller lifecycle + case viewController; + + /// Trigger cleanup via react lifecycle `componentWillUnmount` event sent from js + /// I.e. via `RNIJSComponentWillUnmountNotifiable` + case reactComponentWillUnmount; + + case disabled; + +}; diff --git a/ios/src_library/Temp/RNICleanup/RNIInternalCleanupMode.swift b/ios/src_library/Temp/RNICleanup/RNIInternalCleanupMode.swift new file mode 100644 index 00000000..469366f2 --- /dev/null +++ b/ios/src_library/Temp/RNICleanup/RNIInternalCleanupMode.swift @@ -0,0 +1,40 @@ +// +// RNIInternalCleanupMode.swift +// react-native-ios-utilities +// +// Created by Dominic Go on 10/29/22. +// + +import Foundation + + +public protocol RNIInternalCleanupMode { + /// shadow variable for react prop + var synthesizedInternalCleanupMode: RNICleanupMode { get }; + + /// exported react prop + var internalCleanupMode: String? { get }; + + /// computed property - override behavior for `.automatic` + var cleanupMode: RNICleanupMode { get }; +}; + +// provide default implementation +public extension RNIInternalCleanupMode { + var shouldEnableAttachToParentVC: Bool { + self.cleanupMode == .viewController + }; + + var shouldEnableCleanup: Bool { + self.cleanupMode != .disabled + }; + + var cleanupMode: RNICleanupMode { + get { + switch self.synthesizedInternalCleanupMode { + case .automatic: return .reactComponentWillUnmount; + default: return self.synthesizedInternalCleanupMode; + }; + } + }; +}; diff --git a/ios/src_library/Temp/RNIError/RNIError.swift b/ios/src_library/Temp/RNIError/RNIError.swift new file mode 100644 index 00000000..a01a2f46 --- /dev/null +++ b/ios/src_library/Temp/RNIError/RNIError.swift @@ -0,0 +1,59 @@ +// +// RNINavigatorError.swift +// react-native-ios-navigator +// +// Created by Dominic Go on 9/11/21. +// + +import Foundation + + +open class RNIBaseError: Error where E.RawValue == String { + + public var code: E; + public let domain: String; + + public let message: String?; + public let debug: String?; + + public init( + code: E, + domain: String, + message: String? = nil, + debug: String? = nil + ) { + self.code = code; + self.domain = domain; + self.message = message; + self.debug = debug; + }; + + public func createJSONString() -> String? { + let encoder = JSONEncoder(); + + guard let data = try? encoder.encode(self), + let jsonString = String(data: data, encoding: .utf8) + else { return nil }; + + return jsonString; + }; +}; + +// ---------------- +// MARK:- Encodable +// ---------------- + +extension RNIBaseError: Encodable { + enum CodingKeys: String, CodingKey { + case code, domain, message, debug; + }; + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self); + + try container.encode(self.code.rawValue, forKey: .code); + try container.encode(self.domain, forKey: .domain); + try container.encode(self.message, forKey: .message); + try container.encode(self.debug, forKey: .debug); + }; +}; diff --git a/ios/src_library/Temp/RNIError/RNIGenericError.swift b/ios/src_library/Temp/RNIError/RNIGenericError.swift new file mode 100644 index 00000000..af4895f3 --- /dev/null +++ b/ios/src_library/Temp/RNIError/RNIGenericError.swift @@ -0,0 +1,25 @@ +// +// RNIGenericError.swift +// react-native-ios-utilities +// +// Created by Dominic Go on 4/21/22. +// + +import Foundation + + +public class RNIGenericError: RNIBaseError { + + init( + code: RNIGenericErrorCode, + message: String? = nil, + debug: String? = nil + ) { + super.init( + code: code, + domain: "react-native-ios-utilities", + message: message, + debug: debug + ); + }; +}; diff --git a/ios/src_library/Temp/RNIError/RNIGenericErrorCode.swift b/ios/src_library/Temp/RNIError/RNIGenericErrorCode.swift new file mode 100644 index 00000000..428cbc52 --- /dev/null +++ b/ios/src_library/Temp/RNIError/RNIGenericErrorCode.swift @@ -0,0 +1,16 @@ +// +// RNIGenericErrorCode.swift +// react-native-ios-utilities +// +// Created by Dominic Go on 4/21/22. +// + +import Foundation + + +public enum RNIGenericErrorCode: + String, Codable, CaseIterable, RNIGenericErrorDefaultable { + + case runtimeError, libraryError, reactError, unknownError, + invalidArgument, outOfBounds, invalidReactTag, nilValue; +}; diff --git a/ios/src_library/Temp/RNIError/RNIGenericErrorDefaultable.swift b/ios/src_library/Temp/RNIError/RNIGenericErrorDefaultable.swift new file mode 100644 index 00000000..4c684e1a --- /dev/null +++ b/ios/src_library/Temp/RNIError/RNIGenericErrorDefaultable.swift @@ -0,0 +1,20 @@ +// +// RNIGenericErrorDefaultable.swift +// react-native-ios-utilities +// +// Created by Dominic Go on 8/28/22. +// + +import Foundation + + +public protocol RNIGenericErrorDefaultable { + static var runtimeError : Self { get }; + static var libraryError : Self { get }; + static var reactError : Self { get }; + static var unknownError : Self { get }; + static var invalidArgument: Self { get }; + static var outOfBounds : Self { get }; + static var invalidReactTag: Self { get }; + static var nilValue : Self { get }; +}; diff --git a/ios/src_library/Temp/RNIImage/RNIImageGradientMaker.swift b/ios/src_library/Temp/RNIImage/RNIImageGradientMaker.swift new file mode 100644 index 00000000..682874d7 --- /dev/null +++ b/ios/src_library/Temp/RNIImage/RNIImageGradientMaker.swift @@ -0,0 +1,181 @@ +// +// RNIImageGradientMaker.swift +// react-native-ios-context-menu +// +// Created by Dominic Go on 9/26/22. +// + +import Foundation +import UIKit + + +public struct RNIImageGradientMaker { + public enum PointPresets: String { + case top, bottom, left, right; + case bottomLeft, bottomRight, topLeft, topRight; + + public var cgPoint: CGPoint { + switch self { + case .top : return CGPoint(x: 0.5, y: 0.0); + case .bottom: return CGPoint(x: 0.5, y: 1.0); + + case .left : return CGPoint(x: 0.0, y: 0.5); + case .right: return CGPoint(x: 1.0, y: 0.5); + + case .bottomLeft : return CGPoint(x: 0.0, y: 1.0); + case .bottomRight: return CGPoint(x: 1.0, y: 1.0); + + case .topLeft : return CGPoint(x: 0.0, y: 0.0); + case .topRight: return CGPoint(x: 1.0, y: 0.0); + }; + }; + }; + + public enum DirectionPresets: String { + // horizontal + case leftToRight, rightToLeft; + // vertical + case topToBottom, bottomToTop; + // diagonal + case topLeftToBottomRight, topRightToBottomLeft; + case bottomLeftToTopRight, bottomRightToTopLeft; + + public var point: (start: CGPoint, end: CGPoint) { + switch self { + case .leftToRight: + return (CGPoint(x: 0.0, y: 0.5), CGPoint(x: 1.0, y: 1.5)); + + case .rightToLeft: + return (CGPoint(x: 1.0, y: 0.5), CGPoint(x: 0.0, y: 0.5)); + + case .topToBottom: + return (CGPoint(x: 0.5, y: 1.0), CGPoint(x: 0.5, y: 0.0)); + + case .bottomToTop: + return (CGPoint(x: 0.5, y: 1.0), CGPoint(x: 0.5, y: 0.0)); + + case .topLeftToBottomRight: + return (CGPoint(x: 0.0, y: 0.0), CGPoint(x: 1.0, y: 1.0)); + + case .topRightToBottomLeft: + return (CGPoint(x: 1.0, y: 0.0), CGPoint(x: 0.0, y: 1.0)); + + case .bottomLeftToTopRight: + return (CGPoint(x: 0.0, y: 1.0), CGPoint(x: 1.0, y: 0.0)); + + case .bottomRightToTopLeft: + return (CGPoint(x: 1.0, y: 1.0), CGPoint(x: 0.0, y: 0.0)); + }; + }; + }; + + static private func extractCGPoint(dict: NSDictionary) -> CGPoint? { + guard let x = dict["x"] as? CGFloat, + let y = dict["y"] as? CGFloat + else { return nil }; + + return CGPoint(x: x, y: y); + }; + + static private func extractPoint(dict: NSDictionary, key: String) -> CGPoint? { + if let pointDict = dict[key] as? NSDictionary, + let point = Self.extractCGPoint(dict: pointDict) { + + return point; + + } else if let pointString = dict[key] as? String, + let point = PointPresets(rawValue: pointString) { + + return point.cgPoint; + + } else { + return nil; + }; + } + + public let type: CAGradientLayerType; + + public let colors : [CGColor]; + public let locations : [NSNumber]?; + public let startPoint: CGPoint; + public let endPoint : CGPoint; + + public var size: CGSize; + public let borderRadius: CGFloat; + + public var gradientLayer: CALayer { + let layer = CAGradientLayer(); + + layer.type = self.type; + layer.colors = self.colors; + layer.locations = self.locations; + layer.startPoint = self.startPoint; + layer.endPoint = self.endPoint; + layer.cornerRadius = self.borderRadius; + + return layer; + }; + + public init?(dict: NSDictionary) { + guard let colors = dict["colors"] as? NSArray + else { return nil }; + + self.colors = colors.compactMap { + guard let string = $0 as? String, + let color = UIColor(cssColor: string) + else { return nil }; + + return color.cgColor; + }; + + self.type = { + guard let string = dict["type"] as? String, + let type = CAGradientLayerType(string: string) + else { return .axial }; + + return type; + }(); + + self.locations = { + guard let locations = dict["locations"] as? NSArray else { return nil }; + return locations.compactMap { $0 as? NSNumber }; + }(); + + self.startPoint = Self.extractPoint(dict: dict, key: "startPoint") + ?? PointPresets.top.cgPoint; + + self.endPoint = Self.extractPoint(dict: dict, key: "endPoint") + ?? PointPresets.bottom.cgPoint; + + self.size = CGSize( + width : (dict["width" ] as? CGFloat) ?? 0, + height: (dict["height"] as? CGFloat) ?? 0 + ); + + self.borderRadius = dict["borderRadius"] as? CGFloat ?? 0; + }; + + public mutating func setSizeIfNotSet(_ newSize: CGSize){ + self.size = CGSize( + width : self.size.width <= 0 ? newSize.width : self.size.width, + height: self.size.height <= 0 ? newSize.height : self.size.height + ); + }; + + public func makeImage() -> UIImage { + return UIGraphicsImageRenderer(size: self.size).image { context in + let rect = CGRect(origin: .zero, size: self.size); + + let gradient = self.gradientLayer; + gradient.frame = rect; + gradient.render(in: context.cgContext); + + let clipPath = UIBezierPath( + roundedRect : rect, + cornerRadius: self.borderRadius + ); + + clipPath.addClip(); + }; + }; +}; diff --git a/ios/src_library/Temp/RNIImage/RNIImageItem.swift b/ios/src_library/Temp/RNIImage/RNIImageItem.swift new file mode 100644 index 00000000..9e08d083 --- /dev/null +++ b/ios/src_library/Temp/RNIImage/RNIImageItem.swift @@ -0,0 +1,213 @@ +// +// RNIImageItem.swift +// IosNavigatorExample +// +// Created by Dominic Go on 1/29/21. +// + +import Foundation +import UIKit + + +public class RNIImageItem { + + // MARK: - Properties - Config + // --------------------------- + + public let type: RNIImageType; + public let imageValue: Any?; + public let imageOptions: RNIImageOptions; + + public var defaultSize: CGSize; + + // MARK: Properties - Misc + // ----------------------- + + public let imageConfig: RNIImageConfig; + public var loadedImage: UIImage?; + + // MARK: Properties - Computed + // --------------------------- + + public var baseImage: UIImage? { + switch self.imageConfig { + case let .IMAGE_ASSET(assetName): + return UIImage(named: assetName); + + case let .IMAGE_SYSTEM(imageConfig): + guard #available(iOS 13.0, *) else { return nil }; + return imageConfig.image; + + case let .IMAGE_REQUIRE(imageConfig): + return imageConfig.image; + + case .IMAGE_EMPTY: + return UIImage(); + + case let .IMAGE_RECT(imageConfig): + return imageConfig.makeImage(); + + case let .IMAGE_GRADIENT(imageConfig): + return imageConfig.makeImage(); + + case let .IMAGE_REMOTE_URL(imageConfig): + return imageConfig.image; + }; + }; + + public var imageWithTint: UIImage? { + guard var image = self.baseImage else { return nil }; + guard let tint = self.imageOptions.tint else { return image }; + + let isTintTransparent = tint.rgba.a < 1; + + if isTintTransparent { + let overlay = RNIImageMaker(size: image.size, fillColor: tint, borderRadius: 0); + let overlayImage = overlay.makeImage(); + + return UIGraphicsImageRenderer(size: image.size).image { context in + let rect = CGRect(origin: .zero, size: image.size); + + image.draw(in: rect); + overlayImage.draw(in: rect); + }; + }; + + if image.renderingMode != self.imageOptions.renderingMode { + image = image.withRenderingMode(self.imageOptions.renderingMode) + }; + + if #available(iOS 13.0, *) { + image = image.withTintColor( + tint, + renderingMode: self.imageOptions.renderingMode + ); + }; + + return image; + }; + + public var imageWithRoundedEdges: UIImage? { + guard let image = self.imageWithTint + else { return nil }; + + guard let cornerRadius = self.imageOptions.cornerRadius + else { return image }; + + return UIGraphicsImageRenderer(size: image.size).image { context in + let rect = CGRect(origin: .zero, size: image.size); + + let clipPath = UIBezierPath( + roundedRect : rect, + cornerRadius: cornerRadius + ); + + clipPath.addClip(); + image.draw(in: rect); + }; + }; + + public var image: UIImage? { + self.imageWithRoundedEdges + }; + + public var dictionary: [String: Any] { + var dict: [String: Any] = [ + "type": self.type + ]; + + if let imageValue = self.imageValue { + dict["imageValue"] = imageValue; + }; + + return dict; + }; + + // MARK: - Init + // ----------- + + public init?( + type: RNIImageType, + imageValue: Any?, + imageOptions: NSDictionary?, + imageLoadingConfig: NSDictionary? = nil, + defaultImageSize: CGSize = CGSize(width: 100, height: 100) + ){ + + self.type = type; + self.imageValue = imageValue; + self.defaultSize = defaultImageSize; + + guard let imageConfig: RNIImageConfig = { + switch type { + case .IMAGE_ASSET: + guard let string = imageValue as? String + else { return nil }; + + return .IMAGE_ASSET(assetName: string); + + case .IMAGE_SYSTEM: + guard #available(iOS 13.0, *), + let rawConfig = imageValue as? NSDictionary, + let imageConfig = RNIImageSystemMaker(dict: rawConfig) + else { return nil }; + + return .IMAGE_SYSTEM(config: imageConfig); + + case .IMAGE_REQUIRE: + guard let rawConfig = imageValue as? NSDictionary, + let imageConfig = RNIImageRequireMaker( + dict: rawConfig, + imageLoadingConfig: imageLoadingConfig + ) + else { return nil }; + + return .IMAGE_REQUIRE(config: imageConfig); + + case .IMAGE_EMPTY: + return .IMAGE_EMPTY; + + case .IMAGE_RECT: + guard let rawConfig = imageValue as? NSDictionary, + let imageConfig = RNIImageMaker(dict: rawConfig) + else { return nil }; + + return .IMAGE_RECT(config: imageConfig); + + case .IMAGE_GRADIENT: + guard let rawConfig = imageValue as? NSDictionary, + var imageConfig = RNIImageGradientMaker(dict: rawConfig) + else { return nil }; + + imageConfig.setSizeIfNotSet(defaultImageSize); + return .IMAGE_GRADIENT(config: imageConfig); + + case .IMAGE_REMOTE_URL: + guard let rawConfig = imageValue as? NSDictionary, + let imageConfig = RNIImageRemoteURLMaker( + dict: rawConfig, + imageLoadingConfig: imageLoadingConfig + ) + else { return nil }; + + return .IMAGE_REMOTE_URL(config: imageConfig); + }; + }() else { return nil }; + + self.imageConfig = imageConfig; + self.imageOptions = RNIImageOptions(dict: imageOptions ?? [:]); + }; + + public convenience init?(dict: NSDictionary){ + guard let typeString = dict["type"] as? String, + let type = RNIImageType(rawValue: typeString) + else { return nil }; + + self.init( + type: type, + imageValue: dict["imageValue"], + imageOptions: dict["imageOptions"] as? NSDictionary, + imageLoadingConfig: dict["imageLoadingConfig"] as? NSDictionary + ); + }; +}; diff --git a/ios/src_library/Temp/RNIImage/RNIImageLoadingConfig.swift b/ios/src_library/Temp/RNIImage/RNIImageLoadingConfig.swift new file mode 100644 index 00000000..987ada20 --- /dev/null +++ b/ios/src_library/Temp/RNIImage/RNIImageLoadingConfig.swift @@ -0,0 +1,26 @@ +// +// RNIImageCacheAndLoadingConfig.swift +// react-native-ios-context-menu +// +// Created by Dominic Go on 9/27/22. +// + +import Foundation + + +public protocol RNIImageLoadingConfigurable { + var shouldCache: Bool? { get }; + var shouldLazyLoad: Bool? { get }; +}; + +// TODO: Per file defaults via extension +public struct RNIImageLoadingConfig: RNIImageLoadingConfigurable { + + public let shouldCache: Bool?; + public let shouldLazyLoad: Bool?; + + public init(dict: NSDictionary) { + self.shouldCache = dict["shouldCache"] as? Bool; + self.shouldLazyLoad = dict["shouldLazyLoad"] as? Bool; + }; +}; diff --git a/ios/src_library/Temp/RNIImage/RNIImageMaker.swift b/ios/src_library/Temp/RNIImage/RNIImageMaker.swift new file mode 100644 index 00000000..27896691 --- /dev/null +++ b/ios/src_library/Temp/RNIImage/RNIImageMaker.swift @@ -0,0 +1,59 @@ +// +// RNIImageMaker.swift +// react-native-ios-context-menu +// +// Created by Dominic Go on 9/26/22. +// + +import Foundation +import UIKit + + +public struct RNIImageMaker { + + public let size : CGSize; + public let fillColor : UIColor; + public let borderRadius: CGFloat; + + public init( + size: CGSize, + fillColor: UIColor, + borderRadius: CGFloat + ) { + self.size = size; + self.fillColor = fillColor; + self.borderRadius = borderRadius; + }; + + public init?(dict: NSDictionary) { + guard let width = dict["width" ] as? CGFloat, + let height = dict["height"] as? CGFloat + else { return nil }; + + self.size = CGSize(width: width, height: height); + + guard let fillColorValue = dict["fillColor" ], + let fillColor = UIColor.parseColor(value: fillColorValue) + else { return nil }; + + self.fillColor = fillColor; + + self.borderRadius = dict["borderRadius"] as? CGFloat ?? 0; + }; + + public func makeImage() -> UIImage { + return UIGraphicsImageRenderer(size: self.size).image { context in + let rect = CGRect(origin: .zero, size: self.size); + + let clipPath = UIBezierPath( + roundedRect : rect, + cornerRadius: self.borderRadius + ); + + clipPath.addClip(); + self.fillColor.setFill(); + + context.fill(rect); + }; + }; +}; diff --git a/ios/src_library/Temp/RNIImage/RNIImageOptions.swift b/ios/src_library/Temp/RNIImage/RNIImageOptions.swift new file mode 100644 index 00000000..5ae6a44a --- /dev/null +++ b/ios/src_library/Temp/RNIImage/RNIImageOptions.swift @@ -0,0 +1,35 @@ +// +// RNIImageOptions.swift +// react-native-ios-context-menu +// +// Created by Dominic Go on 9/28/22. +// + +import Foundation + + +public struct RNIImageOptions { + let tint: UIColor?; + let renderingMode: UIImage.RenderingMode; + let cornerRadius: CGFloat?; + + init(dict: NSDictionary){ + self.tint = { + guard let value = dict["tint"], + let color = UIColor.parseColor(value: value) + else { return nil }; + + return color; + }(); + + self.renderingMode = { + guard let string = dict["renderingMode"] as? String, + let mode = UIImage.RenderingMode(string: string) + else { return .automatic }; + + return mode; + }(); + + self.cornerRadius = dict["cornerRadius"] as? CGFloat; + }; +}; diff --git a/ios/src_library/Temp/RNIImage/RNIImageRemoteURLMaker.swift b/ios/src_library/Temp/RNIImage/RNIImageRemoteURLMaker.swift new file mode 100644 index 00000000..51097767 --- /dev/null +++ b/ios/src_library/Temp/RNIImage/RNIImageRemoteURLMaker.swift @@ -0,0 +1,361 @@ +// +// RNIImageRemoteURLMaker.swift +// react-native-ios-context-menu +// +// Created by Dominic Go on 9/27/22. +// + +import Foundation +import UIKit + + +public class RNIImageRemoteURLMaker { + + // MARK: - Embedded Types + // ---------------------- + + /// Note: Unable to synthesize `Equatable` conformance because of `Error` associated value. + public enum State { + + // MARK: - Start State + // ------------------- + + case INITIAL; + + // MARK: - Intermediate State + // -------------------------- + + case LOADING; + + // loading was triggered again because it failed previously + case RETRYING(prevError: Error); + + case LOADED_ERROR (error: Error? = nil); + + // MARK: - Final State + // ------------------- + + case LOADED(image: UIImage); + + // no more remaining retry attempts, don't trigger loading anymore + case LOADED_ERROR_FINAL (error: Error? = nil); + + // MARK: - Computed Properties + // --------------------------- + + var isLoading: Bool { + switch self { + case .LOADING : fallthrough; + case .RETRYING: return true; + + default: return false; + }; + }; + + var error: Error? { + switch self { + case let .LOADED_ERROR(error): return error; + default: return nil; + }; + }; + + var isErrorState: Bool { + switch self { + case .LOADED_ERROR: return true; + default: return false; + }; + }; + + var isFinalState: Bool { + switch self { + case .LOADED : fallthrough; + case .LOADED_ERROR_FINAL: return true; + + default: return false; + }; + }; + }; + + public typealias ImageDidLoadHandler = ( + _ isSuccess: Bool, + _ sender: RNIImageRemoteURLMaker + ) -> Void; + + // MARK: - Class Members + // --------------------- + + public static var imageCache: [String: UIImage] = [:]; + + // MARK: - Properties - Serialized + // ------------------------------- + + public let urlString: String; + + // MARK: - Properties - Derived/Parsed + // ----------------------------------- + + public let url: URL?; + public let imageLoadingConfig: RNIRemoteURLImageLoadingConfig; + + public let fallbackImageConfig: RNIImageItem?; + + // MARK: - Properties + // ------------------ + + public lazy var imageLoader: RCTImageLoaderWithAttributionProtocol? = { + RNIUtilities.sharedBridge?.module(forName: "ImageLoader") as? + RCTImageLoaderWithAttributionProtocol; + }(); + + public var state: State = .INITIAL; + public var loadingAttemptsCount = 0; + + /// Reminder: Use weak self to prevent retain cycle + memory leak + public var onImageDidLoadBlock: ImageDidLoadHandler?; + + // MARK: - Properties - Computed + // ----------------------------- + + private var cachedImage: UIImage? { + guard self.imageLoadingConfig._shouldCache, + let cachedImage = Self.imageCache[self.urlString] + else { return nil }; + + return cachedImage; + }; + + public var shouldRetry: Bool { + let maxRetryAttempts = self.imageLoadingConfig._maxRetryAttempts; + + // Note: negative max retry attempt means infinite retry + return maxRetryAttempts < 0 + ? true + : self.loadingAttemptsCount < maxRetryAttempts; + }; + + // Get image w/o triggering loading logic (i.e. no side effects) + // This will also use the fallback image when appropriate + public var _image: UIImage? { + let fallbackBehavior = self.imageLoadingConfig._fallbackBehavior; + + switch self.state { + case .INITIAL: fallthrough; + case .LOADING: + // A - Use fallback image when the remote image hasn't been loaded yet + switch fallbackBehavior { + case .whileNotLoaded: return self.fallbackImage; + default: return nil; + }; + + case .RETRYING : fallthrough; + case .LOADED_ERROR: + // B - Use fallback image when the remote image hasn't been loaded yet + // due to an error + switch fallbackBehavior { + case .whileNotLoaded: fallthrough; + case .onLoadError : return self.fallbackImage; + + default: return nil; + }; + + case .LOADED_ERROR_FINAL: + // C - Use fallback image when the remote image has failed to load, and + // no more "retry loading" attempts remaining + switch fallbackBehavior { + case .whileNotLoaded : fallthrough; + case .afterFinalAttempt: fallthrough; + case .onLoadError : return self.fallbackImage; + }; + + case .LOADED(image: let image): + return image; + }; + }; + + // Get image + trigger loading logic when not yet loaded + public var image: UIImage? { + switch self.state { + case .INITIAL: + // A - image not loaded yet... + // trigger image loading so it's loaded the next time + self.loadImage(); + return self._image; + + case .LOADED_ERROR: + // B - image loading failed... + // retry loading so it's loaded next time + self.loadImage(); + fallthrough; + + default: + return self._image; + }; + }; + + public var synthesizedURLRequest: URLRequest? { + guard let url = self.url else { return nil }; + return URLRequest(url: url); + }; + + public var fallbackImage: UIImage? { + self.fallbackImageConfig?.image + }; + + // MARK: - Init + // ------------ + + public init?( + dict: NSDictionary, + imageLoadingConfig: NSDictionary?, + onImageDidLoadBlock: ImageDidLoadHandler? = nil + ){ + guard let urlString = dict["url"] as? String + else { return nil }; + + self.urlString = urlString; + self.url = URL(string: urlString); + + self.fallbackImageConfig = { + guard let rawConfig = dict["fallbackImage"] as? NSDictionary + else { return nil }; + + return RNIImageItem(dict: rawConfig); + }(); + + self.imageLoadingConfig = + RNIRemoteURLImageLoadingConfig(dict: imageLoadingConfig ?? [:]); + + self.onImageDidLoadBlock = onImageDidLoadBlock; + + if self.url != nil { + self.setup(); + + } else if self.fallbackImage != nil { + // B - Failed to construct URL instance from string... + // Use fallback image. + self.state = .LOADED_ERROR(); + + } else { + // C - Failed to construct URL instance from string and no fallback image + // is available + return nil; + }; + }; + + // MARK: Functions + // --------------- + + private func setup(){ + let cachedImage = self.cachedImage; + + let shouldLazyLoad = self.imageLoadingConfig.shouldLazyLoad ?? false; + let shouldUseCache = self.imageLoadingConfig.shouldCache ?? false; + + /// Either: + /// * A - no cache exists for the provided url string + /// * B - image caching has been disabled + let hasCachedImage = shouldUseCache && cachedImage != nil; + + let shouldPreloadImage = !shouldLazyLoad && !hasCachedImage; + + if shouldPreloadImage { + // A - Load image in the bg, so it's potentially ready when the image is + // accessed later... + self.loadImage(); + + } else if hasCachedImage { + // B - Use the cached image that matched with the provided url + self.state = .LOADED(image: cachedImage!); + }; + }; + + public func loadImage(){ + // still has retry attempts remaining, and not currently loading + let shouldLoad = + self.shouldRetry && !self.state.isFinalState; + + guard shouldLoad, + let urlRequest = self.synthesizedURLRequest, + let imageLoader = self.imageLoader + else { + return; + }; + + let prevError = self.state.error; + let hasPrevError = prevError != nil; + + self.state = hasPrevError + // A - Retry loading the remote image + ? .RETRYING(prevError: prevError!) + // B - Loading the remote image for the 1st time + : .LOADING; + + self.loadingAttemptsCount += 1; + let prevImage = self._image; + + imageLoader.loadImage(with: urlRequest){ [weak self] in + guard let strongSelf = self else { return }; + + if let error = $0 { + strongSelf.state = strongSelf.shouldRetry + // A - Error Loading - Try again + ? .LOADED_ERROR(error: error) + // B - Error Loading - Final attempt + : .LOADED_ERROR_FINAL(error: error); + + let nextImage = strongSelf._image; + + if !RNIUtilities.compareImages(prevImage, nextImage) { + DispatchQueue.main.async { [weak self] in + guard let strongSelf = self else { return }; + + // failed to load image, but is currently using fallback image, so + // notify that it's using the fallback image as a substitute + strongSelf.onImageDidLoadBlock?(false, strongSelf); + }; + }; + + if strongSelf.imageLoadingConfig._shouldImmediatelyRetryLoading { + strongSelf.loadImage(); + }; + + } else if let image = $1 { + DispatchQueue.main.async { [weak self] in + guard let strongSelf = self else { return }; + + strongSelf.state = .LOADED(image: image); + strongSelf.onImageDidLoadBlock?(true, strongSelf); + + if strongSelf.imageLoadingConfig.shouldCache ?? false { + Self.imageCache[strongSelf.urlString] = image; + }; + }; + }; + }; + }; +}; + +// MARK: - RNIRemoteURLImageLoadingConfig - Defaults +// ------------------------------------------------- + +fileprivate extension RNIRemoteURLImageLoadingConfig { + var _shouldLazyLoad: Bool { + self.shouldLazyLoad ?? false; + }; + + var _shouldCache: Bool { + self.shouldCache ?? false; + }; + + var _maxRetryAttempts: Int { + self.maxRetryAttempts ?? 3; + }; + + var _shouldImmediatelyRetryLoading: Bool { + self.shouldImmediatelyRetryLoading ?? false; + }; + + var _fallbackBehavior: FallbackBehavior { + self.fallbackBehavior ?? .onLoadError; + }; +}; diff --git a/ios/src_library/Temp/RNIImage/RNIImageRequireMaker.swift b/ios/src_library/Temp/RNIImage/RNIImageRequireMaker.swift new file mode 100644 index 00000000..543d7662 --- /dev/null +++ b/ios/src_library/Temp/RNIImage/RNIImageRequireMaker.swift @@ -0,0 +1,75 @@ +// +// RNIImageRequireMaker.swift +// react-native-ios-context-menu +// +// Created by Dominic Go on 10/4/22. +// + +import Foundation + + +public class RNIImageRequireMaker { + static private var imageCache: [String: UIImage] = [:]; + + public let uri: String; + public let imageLoadingConfig: RNIImageLoadingConfig; + + public let rawConfig: NSDictionary; + + public lazy var image: UIImage? = { + let shouldCache = self.imageLoadingConfig._shouldCache; + + if shouldCache, + let cachedImage = Self.imageCache[self.uri] { + + // A - Use cached image + return cachedImage; + }; + + // B - No cached image + let image = RCTConvert.uiImage(self.rawConfig); + + if shouldCache { + Self.imageCache[self.uri] = image; + }; + + return image; + }(); + + public init?( + dict: NSDictionary, + imageLoadingConfig loadingConfigDict: NSDictionary? + ){ + guard let uriString = dict["uri"] as? String + else { return nil }; + + self.uri = uriString; + self.rawConfig = dict; + + self.imageLoadingConfig = + RNIImageLoadingConfig(dict: loadingConfigDict ?? [:]); + + self.preloadImageIfNeeded(); + }; + + private func preloadImageIfNeeded(){ + guard !self.imageLoadingConfig._shouldLazyLoad + else { return }; + + // trigger loading of image + _ = self.image; + }; +}; + +// MARK: - RNIImageLoadingConfig - Defaults +// ---------------------------------------- + +fileprivate extension RNIImageLoadingConfig { + var _shouldLazyLoad: Bool { + self.shouldLazyLoad ?? false; + }; + + var _shouldCache: Bool { + self.shouldCache ?? false; + }; +}; diff --git a/ios/src_library/Temp/RNIImage/RNIImageSystemMaker.swift b/ios/src_library/Temp/RNIImage/RNIImageSystemMaker.swift new file mode 100644 index 00000000..0e0b45bb --- /dev/null +++ b/ios/src_library/Temp/RNIImage/RNIImageSystemMaker.swift @@ -0,0 +1,113 @@ +// +// RNIImageSystemMaker.swift +// react-native-ios-context-menu +// +// Created by Dominic Go on 9/26/22. +// + +import Foundation +import UIKit + + +public struct RNIImageSystemMaker { + public let systemName: String; + + public let pointSize: CGFloat?; + public let weight: String?; + public let scale: String?; + + public let hierarchicalColor: UIColor?; + public let paletteColors: [UIColor]?; + + @available(iOS 13.0, *) + public var symbolConfigs: [UIImage.SymbolConfiguration] { + var configs: [UIImage.SymbolConfiguration] = []; + + if let pointSize = self.pointSize { + configs.append( .init(pointSize: pointSize) ); + }; + + if let string = self.weight, + let weight = UIImage.SymbolWeight(string: string) { + + configs.append( .init(weight: weight) ); + }; + + if let string = self.scale, + let scale = UIImage.SymbolScale(string: string) { + + configs.append( .init(scale: scale) ); + }; + + #if swift(>=5.5) + if #available(iOS 15.0, *), + let color = self.hierarchicalColor { + + configs.append( .init(hierarchicalColor: color) ); + }; + + if #available(iOS 15.0, *), + let colors = self.paletteColors { + + configs.append( .init(paletteColors: colors) ); + }; + #endif + + return configs; + }; + + @available(iOS 13.0, *) + public var symbolConfig: UIImage.SymbolConfiguration? { + var combinedConfig: UIImage.SymbolConfiguration?; + + for config in symbolConfigs { + if let prevCombinedConfig = combinedConfig { + combinedConfig = prevCombinedConfig.applying(config); + + } else { + combinedConfig = config; + }; + }; + + return combinedConfig; + }; + + @available(iOS 13.0, *) + public var image: UIImage? { + if let symbolConfig = symbolConfig { + return UIImage( + systemName: self.systemName, + withConfiguration: symbolConfig + ); + }; + + return UIImage(systemName: self.systemName); + }; + + public init?(dict: NSDictionary){ + guard let systemName = dict["systemName"] as? String + else { return nil }; + + self.systemName = systemName; + + self.pointSize = dict["pointSize"] as? CGFloat; + self.weight = dict["weight" ] as? String; + self.scale = dict["scale" ] as? String; + + self.hierarchicalColor = { + guard let value = dict["hierarchicalColor"], + let color = UIColor.parseColor(value: value) + else { return nil }; + + return color; + }(); + + self.paletteColors = { + guard let items = dict["paletteColors"] as? Array + else { return nil }; + + return items.compactMap { UIColor.parseColor(value: $0) }; + }(); + }; +}; + diff --git a/ios/src_library/Temp/RNIImage/RNIImageType.swift b/ios/src_library/Temp/RNIImage/RNIImageType.swift new file mode 100644 index 00000000..f5e8f6d1 --- /dev/null +++ b/ios/src_library/Temp/RNIImage/RNIImageType.swift @@ -0,0 +1,29 @@ +// +// RNIImageType.swift +// react-native-ios-context-menu +// +// Created by Dominic Go on 9/26/22. +// + +import Foundation + + +public enum RNIImageType: String { + case IMAGE_ASSET; + case IMAGE_SYSTEM; + case IMAGE_REQUIRE; + case IMAGE_EMPTY; + case IMAGE_RECT; + case IMAGE_GRADIENT; + case IMAGE_REMOTE_URL; +}; + +public enum RNIImageConfig { + case IMAGE_ASSET(assetName: String); + case IMAGE_SYSTEM(config: RNIImageSystemMaker); + case IMAGE_REQUIRE(config: RNIImageRequireMaker); + case IMAGE_EMPTY; + case IMAGE_RECT(config: RNIImageMaker); + case IMAGE_GRADIENT(config: RNIImageGradientMaker); + case IMAGE_REMOTE_URL(config: RNIImageRemoteURLMaker); +}; diff --git a/ios/src_library/Temp/RNIImage/RNIRemoteURLImageLoadingConfig.swift b/ios/src_library/Temp/RNIImage/RNIRemoteURLImageLoadingConfig.swift new file mode 100644 index 00000000..8563b51b --- /dev/null +++ b/ios/src_library/Temp/RNIImage/RNIRemoteURLImageLoadingConfig.swift @@ -0,0 +1,52 @@ +// +// RNIRemoteURLImageLoadingConfig.swift +// react-native-ios-context-menu +// +// Created by Dominic Go on 10/2/22. +// + +import Foundation + + + +/// Maps to: `ImageRemoteURLLoadingConfig` +public struct RNIRemoteURLImageLoadingConfig: RNIImageLoadingConfigurable { + + // MARK: Embedded Types + // -------------------- + + /// Maps to: `ImageRemoteURLFallbackBehavior` + public enum FallbackBehavior: String { + case afterFinalAttempt; + case whileNotLoaded; + case onLoadError; + }; + + // MARK: Properties + // ---------------- + + public let shouldCache: Bool?; + public let shouldLazyLoad: Bool?; + + public let maxRetryAttempts: Int?; + public let shouldImmediatelyRetryLoading: Bool?; + public let fallbackBehavior: FallbackBehavior?; + + // MARK: Init + // ---------- + + public init(dict: NSDictionary) { + self.shouldCache = dict["shouldCache"] as? Bool; + self.shouldLazyLoad = dict["shouldLazyLoad"] as? Bool; + + self.maxRetryAttempts = dict["maxRetryAttempts"] as? Int; + self.shouldImmediatelyRetryLoading = dict["shouldImmediatelyRetryLoading"] as? Bool; + + self.fallbackBehavior = { + guard let string = dict["fallbackBehavior"] as? String + else { return nil }; + + return FallbackBehavior(rawValue: string); + }(); + }; +}; diff --git a/ios/src_library/Temp/RNILog/RNILog.swift b/ios/src_library/Temp/RNILog/RNILog.swift new file mode 100644 index 00000000..6e1f46d1 --- /dev/null +++ b/ios/src_library/Temp/RNILog/RNILog.swift @@ -0,0 +1,32 @@ +// +// RCTLog.swift +// nativeUIModulesTest +// +// Created by Dominic Go on 6/27/20. +// + +import Foundation + +public class RNILog { + public static func error(_ message: String, _ file: String=#file, _ line: UInt=#line) { + _RNILog.error(message, file: file, line: line); + }; + + public static func warn(_ message: String, _ file: String=#file, _ line: UInt=#line) { + _RNILog.warn(message, file: file, line: line); + }; + + public static func info(_ message: String, _ file: String=#file, _ line: UInt=#line) { + _RNILog.info(message, file: file, line: line); + }; + + public static func log(_ message: String, _ file: String=#file, _ line: UInt=#line) { + _RNILog.log(message, file: file, line: line); + }; + + public static func trace(_ message: String, _ file: String=#file, _ line: UInt=#line) { + _RNILog.trace(message, file: file, line: line); + }; +}; + + diff --git a/ios/src_library/Temp/RNILog/_RNILog.h b/ios/src_library/Temp/RNILog/_RNILog.h new file mode 100644 index 00000000..17af313e --- /dev/null +++ b/ios/src_library/Temp/RNILog/_RNILog.h @@ -0,0 +1,22 @@ +// +// RCTSwiftLog.h +// nativeUIModulesTest +// +// Created by Dominic Go on 6/27/20. +// + +#import + +@interface _RNILog : NSObject + ++ (void)error:(NSString * _Nonnull)message file:(NSString * _Nonnull)file line:(NSUInteger)line; + ++ (void)warn:(NSString * _Nonnull)message file:(NSString * _Nonnull)file line:(NSUInteger)line; + ++ (void)info:(NSString * _Nonnull)message file:(NSString * _Nonnull)file line:(NSUInteger)line; + ++ (void)log:(NSString * _Nonnull)message file:(NSString * _Nonnull)file line:(NSUInteger)line; + ++ (void)trace:(NSString * _Nonnull)message file:(NSString * _Nonnull)file line:(NSUInteger)line; + +@end diff --git a/ios/src_library/Temp/RNILog/_RNILog.m b/ios/src_library/Temp/RNILog/_RNILog.m new file mode 100644 index 00000000..d776e317 --- /dev/null +++ b/ios/src_library/Temp/RNILog/_RNILog.m @@ -0,0 +1,38 @@ +// +// RCTSwiftLog.m +// nativeUIModulesTest +// +// Created by Dominic Go on 6/27/20. +// + +#import +#import "_RNILog.h" + +@implementation _RNILog + ++ (void)info:(NSString *)message file:(NSString *)file line:(NSUInteger)line +{ + _RCTLogNativeInternal(RCTLogLevelInfo, file.UTF8String, (int)line, @"%@", message); +} + ++ (void)warn:(NSString *)message file:(NSString *)file line:(NSUInteger)line +{ + _RCTLogNativeInternal(RCTLogLevelWarning, file.UTF8String, (int)line, @"%@", message); +} + ++ (void)error:(NSString *)message file:(NSString *)file line:(NSUInteger)line +{ + _RCTLogNativeInternal(RCTLogLevelError, file.UTF8String, (int)line, @"%@", message); +} + ++ (void)log:(NSString *)message file:(NSString *)file line:(NSUInteger)line +{ + _RCTLogNativeInternal(RCTLogLevelInfo, file.UTF8String, (int)line, @"%@", message); +} + ++ (void)trace:(NSString *)message file:(NSString *)file line:(NSUInteger)line +{ + _RCTLogNativeInternal(RCTLogLevelTrace, file.UTF8String, (int)line, @"%@", message); +} + +@end diff --git a/ios/src_library/Temp/RNINavigationEventsReporting/RNINavigationEventsNotifiable.swift b/ios/src_library/Temp/RNINavigationEventsReporting/RNINavigationEventsNotifiable.swift new file mode 100644 index 00000000..66d8a6f4 --- /dev/null +++ b/ios/src_library/Temp/RNINavigationEventsReporting/RNINavigationEventsNotifiable.swift @@ -0,0 +1,14 @@ +// +// RNIContextMenu.swift +// react-native-ios-context-menu +// +// Created by Dominic Go on 2/1/22. +// + +import UIKit + +public protocol RNINavigationEventsNotifiable: NSObject { + + func notifyViewControllerDidPop(sender: RNINavigationEventsReportingViewController); + +}; diff --git a/ios/src_library/Temp/RNINavigationEventsReporting/RNINavigationEventsReportingViewController.swift b/ios/src_library/Temp/RNINavigationEventsReporting/RNINavigationEventsReportingViewController.swift new file mode 100644 index 00000000..9cf98e7b --- /dev/null +++ b/ios/src_library/Temp/RNINavigationEventsReporting/RNINavigationEventsReportingViewController.swift @@ -0,0 +1,65 @@ +// +// RNINavigationEventsReportingViewController.swift +// react-native-ios-context-menu +// +// Created by Dominic Go on 2/1/22. +// + +import UIKit; + + +/// When added as a child VC, it will listen to the parent VC + navigation controller for navigation events +/// and report them to it's delegate and root view. +public class RNINavigationEventsReportingViewController: UIViewController { + + public weak var parentVC: UIViewController?; + public weak var delegate: RNINavigationEventsNotifiable?; + + // MARK: - Init + // ------------ + + public init() { + super.init(nibName: nil, bundle: nil); + }; + + // loaded from a storyboard + public required init?(coder aDecoder: NSCoder) { + super.init(coder: aDecoder); + }; + + // MARK: - Lifecycle + // ----------------- + + public override func viewWillDisappear(_ animated: Bool) { + super.viewWillDisappear(animated); + + guard let navVC = self.navigationController, + let parentVC = self.parentVC + else { return }; + + // if parent VC still exist in the stack, + // then it hasn't been popped yet... + let isParentVCPopped = { + !navVC.viewControllers.contains(parentVC) + }; + + guard isParentVCPopped() + else { return }; + + let notifyDelegate = { + self.delegate?.notifyViewControllerDidPop(sender: self); + }; + + if animated, + let transitionCoordinator = parentVC.transitionCoordinator { + + transitionCoordinator.animate(alongsideTransition: nil){ _ in + guard isParentVCPopped() else { return }; + notifyDelegate(); + }; + + } else { + notifyDelegate(); + }; + }; +}; diff --git a/ios/src_library/Temp/RNIUtilities/RNIUtilities.swift b/ios/src_library/Temp/RNIUtilities/RNIUtilities.swift new file mode 100644 index 00000000..a7456594 --- /dev/null +++ b/ios/src_library/Temp/RNIUtilities/RNIUtilities.swift @@ -0,0 +1,164 @@ +// +// RNIUtilities.swift +// IosNavigatorExample +// +// Created by Dominic Go on 1/9/21. +// + +import Foundation; + + +public class RNIUtilities { + + public static weak var sharedBridge: RCTBridge?; + + public static let osVersion = ProcessInfo().operatingSystemVersion; + + /// If you remove a "react view" from the view hierarchy (e.g. via + /// `removeFromSuperview`), it won't be released, because it's being retained + /// by the `_viewRegistry` ivar in the shared `UIManager` (singleton) instance. + /// + /// The `_viewRegistry` keeps a ref. to all of the "react views" in the app. + /// This explains how you can get a ref. to a view via `viewForReactTag` and + /// `viewForNativeID` (which also means that `reactTag`/`node` is just an index, + /// i.e. it's just a number that gets inc. every-time a "react view" is added). + /// + /// If you are **absolutely sure** that a particular `reactView` is no longer + /// being used, this helper func. will remove `reactView` (and all of it's + /// subviews) in the `_viewRegistry`. + public static func recursivelyRemoveFromViewRegistry(bridge: RCTBridge, reactView: UIView) { + + func getRegistry(forKey key: String) -> NSMutableDictionary? { + return bridge.uiManager?.value(forKey: key) as? NSMutableDictionary; + }; + + /// Get a ref to the `_viewRegistry` ivar in the `RCTUIManager` instance. + /// * Note: Unlike objc properties, ivars are "private" so they aren't + /// automagically exposed/bridged to swift. + /// * Note: key: `NSNumber` (the `reactTag`), and value: `UIView` + guard let viewRegistry = getRegistry(forKey: "_viewRegistry") + else { return }; + + /// The "react tags" of the views that were removed + var removedViewTags: [NSNumber] = []; + + func removeView(_ v: UIView){ + /// if this really is a "react view" then it should have a `reactTag` + if let reactTag = v.reactTag, + viewRegistry[reactTag] != nil { + + removedViewTags.append(reactTag); + + /// remove from view hierarchy + v.removeFromSuperview(); + + /// remove this "react view" from the registry + viewRegistry.removeObject(forKey: reactTag); + }; + + /// remove other subviews... + v.subviews.forEach { + removeView($0); + }; + + /// remove other react subviews... + v.reactSubviews()?.forEach { + removeView($0); + }; + }; + + func removeShadowViews(){ + /// Get a ref to the `_shadowViewRegistry` ivar in the `RCTUIManager` instance. + /// Note: Execute on "RCT thread" (i.e. "com.facebook.react.ShadowQueue") + guard let shadowViewRegistry = getRegistry(forKey: "_shadowViewRegistry") + else { return }; + + for reactTag in removedViewTags { + shadowViewRegistry.removeObject(forKey: reactTag); + }; + }; + + DispatchQueue.main.async { + // start recursively removing views... + removeView(reactView); + + // remove shadow views... + RCTExecuteOnUIManagerQueue { + removeShadowViews(); + }; + }; + }; + + /// Recursive climb the responder chain until `T` is found. + /// Useful for finding the corresponding view controller of a view. + public static func getParent(responder: UIResponder, type: T.Type) -> T? { + var parentResponder: UIResponder? = responder; + + while parentResponder != nil { + parentResponder = parentResponder?.next; + + if let parent = parentResponder as? T { + return parent; + }; + }; + + return nil; + }; + + public static func getView( + forNode node: NSNumber, + type: T.Type, + bridge: RCTBridge? + ) -> T? { + guard let bridge = bridge, + let view = bridge.uiManager?.view(forReactTag: node) + else { return nil }; + + return view as? T; + }; + + public static func recursivelyGetAllSubviews(for view: UIView) -> [UIView] { + var views: [UIView] = []; + + for subview in view.subviews { + views += Self.recursivelyGetAllSubviews(for: subview); + views.append(subview); + }; + + return views; + }; + + public static func recursivelyGetAllSuperViews(for view: UIView) -> [UIView] { + var views: [UIView] = []; + + if let parentView = view.superview { + views.append(parentView); + views += Self.recursivelyGetAllSuperViews(for: parentView); + }; + + return views; + }; + + public static func compareImages(_ a: UIImage?, _ b: UIImage?) -> Bool { + if (a == nil && b == nil){ + // both are nil, equal + return true; + + } else if a == nil || b == nil { + // one is nil, not equal + return false; + + } else if a! === b! { + // same ref to the object, true + return true; + + } else if a!.size == b!.size { + // size diff, not equal + return true; + }; + + // compare raw data + return a!.isEqual(b!); + }; +}; + diff --git a/ios/src_library/Temp/RNIUtilities/RNIUtilitiesModule.m b/ios/src_library/Temp/RNIUtilities/RNIUtilitiesModule.m new file mode 100644 index 00000000..fe3e605e --- /dev/null +++ b/ios/src_library/Temp/RNIUtilities/RNIUtilitiesModule.m @@ -0,0 +1,14 @@ +// +// RNIUtilitiesModule.m +// react-native-ios-context-menu +// +// Created by Dominic Go on 9/27/22. +// + +#import "React/RCTBridgeModule.h" + +@interface RCT_EXTERN_MODULE(RNIUtilitiesModule, NSObject) + +RCT_EXTERN_METHOD(initialize:(NSDictionary *)params); + +@end diff --git a/ios/src_library/Temp/RNIUtilities/RNIUtilitiesModule.swift b/ios/src_library/Temp/RNIUtilities/RNIUtilitiesModule.swift new file mode 100644 index 00000000..e0a99cd5 --- /dev/null +++ b/ios/src_library/Temp/RNIUtilities/RNIUtilitiesModule.swift @@ -0,0 +1,28 @@ +// +// RNIUtilitiesModule.swift +// react-native-ios-context-menu +// +// Created by Dominic Go on 9/27/22. +// + +import Foundation + + +@objc(RNIUtilitiesModule) +internal class RNIUtilitiesModule: NSObject { + + @objc internal var bridge: RCTBridge! { + willSet { + RNIUtilities.sharedBridge = newValue; + } + }; + + @objc internal static func requiresMainQueueSetup() -> Bool { + // run init in bg thread + return false; + }; + + @objc internal func initialize(_ params: NSDictionary){ + // no-op + }; +}; diff --git a/ios/src_library/Temp/RNIWrapperView/RNIWrapperView.swift b/ios/src_library/Temp/RNIWrapperView/RNIWrapperView.swift new file mode 100644 index 00000000..d4e0ee0b --- /dev/null +++ b/ios/src_library/Temp/RNIWrapperView/RNIWrapperView.swift @@ -0,0 +1,273 @@ +// +// RNIWrapperView.swift +// IosNavigatorExample +// +// Created by Dominic Go on 2/1/21. +// + +import UIKit + + +/// Holds react views that have been detached, and are no longer managed by RN. +public class RNIWrapperView: UIView { + + public static var detachedViews = NSMapTable.init( + keyOptions: .copyIn, + valueOptions: .weakMemory + ); + + // MARK: - Properties + // ------------------ + + public private(set) var bridge: RCTBridge!; + + public weak var delegate: RNIWrapperViewEventsNotifiable?; + + /// When `shouldAutoDetachSubviews` is enabled, all the child views that were removed from + /// its parent will be stored here. + /// + /// This is only usually used when `isDummyView` is enabled. + public var reactViews: [UIView] = []; + + public var touchHandlers: Dictionary = [:]; + + // MARK: - Properties - Flags + // -------------------------- + + /// Whether or not `cleanup` was triggered. + public private(set) var didTriggerCleanup = false; + + /// Set this property to `true` before moving this view somewhere else (i.e. + /// before calling `removeFromSuperView`). + /// + /// Setting this property to `true` will prevent triggering `cleanup` when removing this view + /// from it's parent view... + /// + /// After you've finished moving this view, set this back to `false`. + public var isMovingToParent = false; + + public var shouldDelayAutoCleanupOnJSUnmount = true; + + // MARK: - RN Exported Props - Config - Lifecycle Related + // ------------------------------------------------------ + + /// When this prop is set to `true`, the JS component will trigger + /// `shouldNotifyComponentWillUnmount` during `componentWillUnmount`. + @objc public private(set) var shouldNotifyComponentWillUnmount: Bool = false; + + /// This property determines whether `cleanup` should be called when + /// `shouldNotifyComponentWillUnmount` is called. Defaults to: `true`. + /// + /// When `shouldNotifyComponentWillUnmount` prop is true, the JS component + /// will notify it's corresponding native view that it'll be unmounted (i.e. + /// via calling the `shouldNotifyComponentWillUnmount` method). + /// + /// * When a react view is "detached" (i.e. when `removeFromSuperView` is called), + /// the react view is on it's own (the view will leak since it won't be removed + /// when the corresponding view comp. un-mounts). + /// + /// * To get around this, the js comp. notifies the native view that it's + /// going to be unmount in `componentWillUnmount` react lifecycle. + /// + /// * This also fixes the issue where the js comp. has already been unmounted, + /// but it's corresponding native view is still being used. + /// + @objc public private(set) var shouldAutoCleanupOnJSUnmount = false; + + /// Determines whether `cleanup` is called when this view is removed from the + /// view hierarchy (i.e. when the window ref. becomes nil). + @objc public private(set) var shouldAutoCleanupOnWindowNil = false; + + /// Determines whether `layoutSubviews` will automatically trigger + /// `notifyForBoundsChange`. Defaults to `true`. + /// + /// * If the layout size is determined from the react/js side, set this to `false`. + /// + /// * Otherwise if the layout size is determined from the native side (e.g. via + /// the view controller, etc.) then set this to `true`. + @objc public private(set) var shouldAutoSetSizeOnLayout = false; + + // MARK: - RN Exported Props - Config - "Dummy View"-Related + // --------------------------------------------------------- + + /// When set to `true`, the view itself is not the one that's being used for content, as such its a + /// "dummy" view (i.e. its not going to be displayed or used). + /// + /// In this mode, it's child views are the ones that are being used for content, and the parent view will + /// usually get removed from the view hierarchy. + @objc public private(set) var isDummyView = false; + + /// When enabled, the child views will be automatically removed from it's parent, and will be stored in + /// the `reactViews` property. + /// + /// This is usually enabled together with `isDummyView`. + @objc public private(set) var shouldAutoDetachSubviews = false; + + // MARK: - RN Exported Props - Config - Touch Handlers + // --------------------------------------------------- + + /// If you are planning on removing the parent view (i.e. this view instance) from the view hierarchy via + /// calling `removeFromSuperview`, and you still want it to receive touch events , then set this + /// property to `true`. + /// + /// When in dummy mode, `shouldAutoDetachSubviews` is usually also enabled. + @objc public private(set) var shouldCreateTouchHandlerForParentView = false; + + /// If you are planning on removing the subviews from the view hierarchy (i.e. using "dummy view" mode), + /// and you still want them to receive touch event, then set this property to `true`. + @objc public private(set) var shouldCreateTouchHandlerForSubviews = false; + + + // MARK: - Init/Lifecycle + // --------------------- + + init(bridge: RCTBridge) { + super.init(frame: CGRect()); + self.bridge = bridge; + + if self.shouldCreateTouchHandlerForParentView { + self.touchHandlers[self.reactTag] = { + let handler = RCTTouchHandler(bridge: self.bridge)!; + handler.attach(to: self); + + return handler; + }(); + }; + }; + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented"); + }; + + public override func layoutSubviews() { + super.layoutSubviews(); + + if self.shouldAutoSetSizeOnLayout { + self.notifyForBoundsChange(size: self.bounds.size); + }; + }; + + public override func didMoveToWindow() { + + let isMovingToWindowNil = self.window == nil; + + let isOrphaned = self.isDummyView + ? self.reactViews.allSatisfy { $0.superview == nil } + : self.superview == nil; + + /// A: Prevent `cleanup` when changing parent views + /// B: Only trigger cleanup when moving to a `nil` window + let shouldTriggerCleanup = + !self.isMovingToParent && isMovingToWindowNil && isOrphaned; + + if shouldTriggerCleanup { + self.cleanup(); + }; + }; + + public override func removeFromSuperview() { + super.removeFromSuperview(); + Self.detachedViews.setObject(self, forKey: self.reactTag); + } + + // MARK: - React Lifecycle + // ---------------------- + + public override func insertReactSubview(_ subview: UIView!, at atIndex: Int) { + super.insertSubview(subview, at: atIndex); + + if self.shouldAutoDetachSubviews { + self.reactViews.append(subview); + subview.removeFromSuperview(); + }; + + if self.shouldCreateTouchHandlerForSubviews { + self.touchHandlers[subview.reactTag] = { + let handler = RCTTouchHandler(bridge: self.bridge); + handler?.attach(to: subview); + + return handler; + }(); + }; + }; + + // MARK: - Functions + // ------------------ + + public func notifyForBoundsChange(size: CGSize){ + if self.isDummyView { + self.notifyForBoundsChangeForContent(size: size); + + } else { + self.notifyForBoundsChangeForWrapper(size: size); + }; + }; + + public func notifyForBoundsChangeForWrapper(size: CGSize){ + guard let bridge = self.bridge else { return }; + bridge.uiManager.setSize(size, for: self); + }; + + public func notifyForBoundsChangeForContent(size: CGSize){ + guard let bridge = self.bridge + else { return }; + + let views = self.isDummyView + ? self.reactViews + : self.subviews; + + for view in views { + bridge.uiManager.setSize(size, for: view); + }; + }; + + // MARK: - Commands For Module + // -------------------------- + + /// Called by `RNIWrapperViewModule.notifyComponentWillUnmount` + public func onJSComponentWillUnmount(isManuallyTriggered: Bool){ + self.delegate?.onJSComponentWillUnmount( + sender: self, + isManuallyTriggered: isManuallyTriggered + ); + + guard self.shouldAutoCleanupOnJSUnmount else { return }; + + if self.shouldDelayAutoCleanupOnJSUnmount { + DispatchQueue.main.asyncAfter(deadline: .now() + 1) { [weak self] in + self?.cleanup(); + }; + + } else { + self.cleanup(); + }; + }; +}; + + +// MARK: - RNICleanable +// -------------------- + +extension RNIWrapperView: RNICleanable { + + public func cleanup(){ + guard !self.didTriggerCleanup else { return }; + self.didTriggerCleanup = true; + + let viewsToCleanup = self.reactViews + [self]; + + for view in viewsToCleanup { + if let touchHandler = self.touchHandlers[view.reactTag] { + touchHandler.detach(from: view); + }; + + RNIUtilities.recursivelyRemoveFromViewRegistry( + bridge: self.bridge, + reactView: view + ); + }; + + self.touchHandlers.removeAll(); + self.reactViews.removeAll(); + }; +}; diff --git a/ios/src_library/Temp/RNIWrapperView/RNIWrapperViewEventsNotifiable.swift b/ios/src_library/Temp/RNIWrapperView/RNIWrapperViewEventsNotifiable.swift new file mode 100644 index 00000000..5d9268a5 --- /dev/null +++ b/ios/src_library/Temp/RNIWrapperView/RNIWrapperViewEventsNotifiable.swift @@ -0,0 +1,16 @@ +// +// RNIWrapperViewEventsNotifiable.swift +// react-native-ios-context-menu +// +// Created by Dominic Go on 7/17/22. +// + +import Foundation + + +public protocol RNIWrapperViewEventsNotifiable: AnyObject { + func onJSComponentWillUnmount( + sender: RNIWrapperView, + isManuallyTriggered: Bool + ); +}; diff --git a/ios/src_library/Temp/RNIWrapperView/RNIWrapperViewManager.m b/ios/src_library/Temp/RNIWrapperView/RNIWrapperViewManager.m new file mode 100644 index 00000000..2e434632 --- /dev/null +++ b/ios/src_library/Temp/RNIWrapperView/RNIWrapperViewManager.m @@ -0,0 +1,28 @@ +// +// RNIWrapperViewManager.m +// IosNavigatorExample +// +// Created by Dominic Go on 2/1/21. +// + +#import + +@interface RCT_EXTERN_MODULE(RNIWrapperViewManager, RCTViewManager) + +// MARK: - Export Props - Values +// ----------------------------- + +RCT_EXPORT_VIEW_PROPERTY(shouldNotifyComponentWillUnmount, BOOL); +RCT_EXPORT_VIEW_PROPERTY(shouldAutoCleanupOnJSUnmount, BOOL); +RCT_EXPORT_VIEW_PROPERTY(shouldAutoCleanupOnWindowNil, BOOL); +RCT_EXPORT_VIEW_PROPERTY(shouldAutoSetSizeOnLayout, BOOL); + +RCT_EXPORT_VIEW_PROPERTY(isDummyView, BOOL); +RCT_EXPORT_VIEW_PROPERTY(shouldAutoDetachSubviews, BOOL); + +RCT_EXPORT_VIEW_PROPERTY(shouldCreateTouchHandlerForParentView, BOOL); +RCT_EXPORT_VIEW_PROPERTY(shouldCreateTouchHandlerForSubviews, BOOL); + + + +@end diff --git a/ios/src_library/Temp/RNIWrapperView/RNIWrapperViewManager.swift b/ios/src_library/Temp/RNIWrapperView/RNIWrapperViewManager.swift new file mode 100644 index 00000000..a3704245 --- /dev/null +++ b/ios/src_library/Temp/RNIWrapperView/RNIWrapperViewManager.swift @@ -0,0 +1,37 @@ +// +// RNIWrapperViewManager.swift +// IosNavigatorExample +// +// Created by Dominic Go on 2/1/21. +// + +import Foundation + +@objc(RNIWrapperViewManager) +internal class RNIWrapperViewManager: RCTViewManager { + + static var sharedBridge: RCTBridge?; + + // MARK: - RN Module Setup + // ----------------------- + + override static func requiresMainQueueSetup() -> Bool { + // run init in bg thread + return false; + }; + + override func view() -> UIView! { + // save a ref to this module's RN bridge instance + if Self.sharedBridge == nil { + Self.sharedBridge = self.bridge; + }; + + return RNIWrapperView(bridge: self.bridge); + }; + + func invalidate(){ + /// reset ref to RCTBridge instance + Self.sharedBridge = nil; + }; +}; + diff --git a/ios/src_library/Temp/RNIWrapperView/RNIWrapperViewModule.m b/ios/src_library/Temp/RNIWrapperView/RNIWrapperViewModule.m new file mode 100644 index 00000000..3268f775 --- /dev/null +++ b/ios/src_library/Temp/RNIWrapperView/RNIWrapperViewModule.m @@ -0,0 +1,15 @@ +// +// RNIWrapperViewModule.m +// react-native-ios-navigator +// +// Created by Dominic Go on 8/3/21. +// + +#import "React/RCTBridgeModule.h" + +@interface RCT_EXTERN_MODULE(RNIWrapperViewModule, NSObject) + +RCT_EXTERN_METHOD(notifyComponentWillUnmount:(nonnull NSNumber *)node + params:(NSDictionary *)params); + +@end diff --git a/ios/src_library/Temp/RNIWrapperView/RNIWrapperViewModule.swift b/ios/src_library/Temp/RNIWrapperView/RNIWrapperViewModule.swift new file mode 100644 index 00000000..109c0696 --- /dev/null +++ b/ios/src_library/Temp/RNIWrapperView/RNIWrapperViewModule.swift @@ -0,0 +1,54 @@ +// +// RNIWrapperViewModule.swift +// react-native-ios-navigator +// +// Created by Dominic Go on 8/3/21. +// + +import Foundation + +@objc(RNIWrapperViewModule) +internal class RNIWrapperViewModule: NSObject { + + @objc public private(set) var bridge: RCTBridge!; + + @objc static func requiresMainQueueSetup() -> Bool { + // run init in bg thread + return false; + }; + + func getWrapperView(_ node: NSNumber) -> RNIWrapperView? { + return RNIUtilities.getView( + forNode: node, + type : RNIWrapperView.self, + bridge : self.bridge + ); + }; + + // MARK: - Module Commands: Navigator + // --------------------------------- + + @objc func notifyComponentWillUnmount( + _ node: NSNumber, + params: NSDictionary + ){ + DispatchQueue.main.async { + // get `RNIWrapperView` instance that matches node/reactTag + guard let wrapperView = self.getWrapperView(node) else { + #if DEBUG + print( + "LOG - ViewManager, RNIWrapperViewModule: notifyComponentWillUnmount" + + " - for node: \(node)" + + " - no corresponding view found for node" + + " - the view might have already been unmounted..." + ); + #endif + return; + }; + + wrapperView.onJSComponentWillUnmount( + isManuallyTriggered: params["isManuallyTriggered"] as? Bool ?? false + ); + }; + }; +};