From 48e9f6e38213c533286d83e426346471d507467d Mon Sep 17 00:00:00 2001 From: Iván Ávalos Date: Thu, 3 Feb 2022 02:33:50 -0600 Subject: WIP: rewrote MapWrapperView + MapView as unified UIKit view controller --- iosApp/iosApp.xcodeproj/project.pbxproj | 50 ++++- iosApp/iosApp/Map/MapView.swift | 307 ++----------------------------- iosApp/iosApp/Map/MapViewController.xib | 104 +++++++++++ iosApp/iosApp/Map/MapWrapperView.swift | 87 --------- iosApp/iosApp/Map/UnitMapView.swift | 6 +- iosApp/iosApp/Shared/HyperlinkText.swift | 186 ++++++++----------- 6 files changed, 242 insertions(+), 498 deletions(-) create mode 100644 iosApp/iosApp/Map/MapViewController.xib delete mode 100644 iosApp/iosApp/Map/MapWrapperView.swift diff --git a/iosApp/iosApp.xcodeproj/project.pbxproj b/iosApp/iosApp.xcodeproj/project.pbxproj index f155c39..9f2f41f 100644 --- a/iosApp/iosApp.xcodeproj/project.pbxproj +++ b/iosApp/iosApp.xcodeproj/project.pbxproj @@ -19,10 +19,11 @@ E33A236E27A7545500DD647F /* WhirlyGlobeMaplyComponent.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = E33A236B27A7543700DD647F /* WhirlyGlobeMaplyComponent.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; E33A237127A7553500DD647F /* MapView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E33A237027A7553500DD647F /* MapView.swift */; }; E33A237327A7581A00DD647F /* Utils.swift in Sources */ = {isa = PBXBuildFile; fileRef = E33A237227A7581A00DD647F /* Utils.swift */; }; - E34A2F4427A77D9500AD8AEB /* MapWrapperView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E34A2F4327A77D9400AD8AEB /* MapWrapperView.swift */; }; E34A2F4827A7878200AD8AEB /* HyperlinkText.swift in Sources */ = {isa = PBXBuildFile; fileRef = E34A2F4727A7878200AD8AEB /* HyperlinkText.swift */; }; E34A2F4B27A7881200AD8AEB /* SwiftUIFlowLayout in Frameworks */ = {isa = PBXBuildFile; productRef = E34A2F4A27A7881200AD8AEB /* SwiftUIFlowLayout */; }; E34A2F4D27A7DB2200AD8AEB /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = E34A2F4C27A7DB2200AD8AEB /* Localizable.strings */; }; + E36DF77B27AB740C003C561C /* MapViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E36DF77927AB740C003C561C /* MapViewController.swift */; }; + E36DF77C27AB740C003C561C /* MapViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = E36DF77A27AB740C003C561C /* MapViewController.xib */; }; E38F241527A242870069FC45 /* Inject.swift in Sources */ = {isa = PBXBuildFile; fileRef = E38F241427A242870069FC45 /* Inject.swift */; }; E38F241727A242C70069FC45 /* Resolver.swift in Sources */ = {isa = PBXBuildFile; fileRef = E38F241627A242C70069FC45 /* Resolver.swift */; }; E38F241C27A26DD70069FC45 /* RootViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E38F241B27A26DD70069FC45 /* RootViewModel.swift */; }; @@ -61,9 +62,10 @@ E33A236B27A7543700DD647F /* WhirlyGlobeMaplyComponent.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = WhirlyGlobeMaplyComponent.xcframework; sourceTree = ""; }; E33A237027A7553500DD647F /* MapView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapView.swift; sourceTree = ""; }; E33A237227A7581A00DD647F /* Utils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Utils.swift; sourceTree = ""; }; - E34A2F4327A77D9400AD8AEB /* MapWrapperView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapWrapperView.swift; sourceTree = ""; }; E34A2F4727A7878200AD8AEB /* HyperlinkText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HyperlinkText.swift; sourceTree = ""; }; E34A2F4C27A7DB2200AD8AEB /* Localizable.strings */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.strings; path = Localizable.strings; sourceTree = ""; }; + E36DF77927AB740C003C561C /* MapViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MapViewController.swift; sourceTree = ""; }; + E36DF77A27AB740C003C561C /* MapViewController.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = MapViewController.xib; sourceTree = ""; }; E38F241427A242870069FC45 /* Inject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Inject.swift; sourceTree = ""; }; E38F241627A242C70069FC45 /* Resolver.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Resolver.swift; sourceTree = ""; }; E38F241B27A26DD70069FC45 /* RootViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootViewModel.swift; sourceTree = ""; }; @@ -119,6 +121,7 @@ children = ( E34A2F4C27A7DB2200AD8AEB /* Localizable.strings */, E33A235E27A4FD1C00DD647F /* Map */, + E35A078427AB615F00F24D71 /* Details */, E39ABC4427A4EBB000965D05 /* Devices */, E38F241A27A2659C0069FC45 /* Units */, E3E77EE1279D43C000150070 /* Shared */, @@ -144,11 +147,43 @@ children = ( E33A235F27A4FD2C00DD647F /* UnitMapView.swift */, E33A237027A7553500DD647F /* MapView.swift */, - E34A2F4327A77D9400AD8AEB /* MapWrapperView.swift */, + E36DF77927AB740C003C561C /* MapViewController.swift */, + E36DF77A27AB740C003C561C /* MapViewController.xib */, ); path = Map; sourceTree = ""; }; + E35A078427AB615F00F24D71 /* Details */ = { + isa = PBXGroup; + children = ( + E35A078727AB619700F24D71 /* Reports */, + E35A078627AB619000F24D71 /* Information */, + E35A078527AB618700F24D71 /* Commands */, + ); + path = Details; + sourceTree = ""; + }; + E35A078527AB618700F24D71 /* Commands */ = { + isa = PBXGroup; + children = ( + ); + path = Commands; + sourceTree = ""; + }; + E35A078627AB619000F24D71 /* Information */ = { + isa = PBXGroup; + children = ( + ); + path = Information; + sourceTree = ""; + }; + E35A078727AB619700F24D71 /* Reports */ = { + isa = PBXGroup; + children = ( + ); + path = Reports; + sourceTree = ""; + }; E38F241A27A2659C0069FC45 /* Units */ = { isa = PBXGroup; children = ( @@ -229,6 +264,7 @@ TargetAttributes = { 7555FF7A242A565900829871 = { CreatedOnToolsVersion = 11.3.1; + LastSwiftMigration = 1320; }; }; }; @@ -263,6 +299,7 @@ 058557D9273AAEEB004C7B11 /* Preview Assets.xcassets in Resources */, E34A2F4D27A7DB2200AD8AEB /* Localizable.strings in Resources */, 058557BB273AAA24004C7B11 /* Assets.xcassets in Resources */, + E36DF77C27AB740C003C561C /* MapViewController.xib in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -300,12 +337,12 @@ E38F241527A242870069FC45 /* Inject.swift in Sources */, E39ABC4827A4EDEC00965D05 /* DeviceRow.swift in Sources */, E38F241C27A26DD70069FC45 /* RootViewModel.swift in Sources */, + E36DF77B27AB740C003C561C /* MapViewController.swift in Sources */, E33A237327A7581A00DD647F /* Utils.swift in Sources */, E3E77EE6279E6CE400150070 /* FlowCollector.swift in Sources */, E33A236527A530F300DD647F /* SmallLabelStyle.swift in Sources */, E39ABC4327A4E88C00965D05 /* UnitsViewModel.swift in Sources */, E34A2F4827A7878200AD8AEB /* HyperlinkText.swift in Sources */, - E34A2F4427A77D9500AD8AEB /* MapWrapperView.swift in Sources */, 2152FB042600AC8F00CF470E /* iOSApp.swift in Sources */, E39ABC4627A4EBD500965D05 /* DevicesView.swift in Sources */, E38F242027A27B550069FC45 /* LoadingView.swift in Sources */, @@ -437,6 +474,7 @@ isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\""; DEVELOPMENT_TEAM = 358YRZ9P3L; @@ -458,6 +496,8 @@ ); PRODUCT_BUNDLE_IDENTIFIER = mx.trackermap.TrackerMap; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; @@ -467,6 +507,7 @@ isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; CODE_SIGN_STYLE = Automatic; DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\""; DEVELOPMENT_TEAM = 358YRZ9P3L; @@ -488,6 +529,7 @@ ); PRODUCT_BUNDLE_IDENTIFIER = mx.trackermap.TrackerMap; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = ""; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; diff --git a/iosApp/iosApp/Map/MapView.swift b/iosApp/iosApp/Map/MapView.swift index 572db64..6b0fa20 100644 --- a/iosApp/iosApp/Map/MapView.swift +++ b/iosApp/iosApp/Map/MapView.swift @@ -21,310 +21,35 @@ import WhirlyGlobeMaplyComponent import shared struct MapView: UIViewControllerRepresentable { - typealias UIViewControllerType = OurMaplyViewController + typealias UIViewControllerType = MapViewController - @Binding var mapLayer: MapLayer + @Binding var layer: MapLayer @Binding var markers: [Marker] - var markerCallback: ((Int32?) -> Void)? - var link: BaseMapLink + var markerCallback: MarkerCallback? - class Coordinator: NSObject, MaplyViewControllerDelegate { - var parent: MapView - var uiViewController: OurMaplyViewController? - var loader: MaplyQuadImageLoader? = nil - - // Source: https://stackoverflow.com/questions/65923718 - var cancellable: AnyCancellable? - var link: BaseMapLink? { - didSet { - cancellable = link?.$action.sink(receiveValue: { action in - guard let action = action else { - return - } - self.uiViewController?.action(action) - }) - } - } - - init(_ uiViewController: MapView) { - self.parent = uiViewController - } - - func maplyViewController(_ viewC: MaplyViewController, - didTapAt coord: MaplyCoordinate) { - if let callback = parent.markerCallback { - callback(nil) - } - } - - func maplyViewController(_ viewC: MaplyViewController, - didSelect selectedObj: NSObject, - atLoc coord: MaplyCoordinate, - onScreen screenPt: CGPoint) { - if let marker = selectedObj as? MaplyScreenMarker { - if let id = marker.userObject as? Int32 { - if let callback = parent.markerCallback { - callback(id) - } - } - } - } + class Coordinator { + var shouldCenter: Bool = true } func makeCoordinator() -> Coordinator { - Coordinator(self) + return Coordinator() } - func makeUIViewController(context: Context) -> OurMaplyViewController { - let mapViewController = OurMaplyViewController(mapType: .typeFlat) - mapViewController.delegate = context.coordinator - - let tileInfo = Utils.tileInfoFrom(layer: mapLayer) - mapViewController.setZoomLimits(minZoom: mapLayer.minZoom, - maxZoom: mapLayer.maxZoom) - - let sampleParams = MaplySamplingParams() - sampleParams.coordSys = MaplySphericalMercator(webStandard: ()) - sampleParams.coverPoles = true - sampleParams.edgeMatching = true - sampleParams.minZoom = tileInfo.minZoom - sampleParams.maxZoom = tileInfo.maxZoom - sampleParams.singleLevel = true - sampleParams.maxTiles = 25 - - let loader = MaplyQuadImageLoader(params: sampleParams, tileInfo: tileInfo, viewC: mapViewController) - loader?.baseDrawPriority = kMaplyImageLayerDrawPriorityDefault - loader?.imageFormat = .imageUShort565 - context.coordinator.loader = loader - - DispatchQueue.main.async { - let point = MaplyCoordinateMakeWithDegrees(-100.36, 23.191) - mapViewController.focusOn(point: point, height: 0.4, animated: false) - } - - return mapViewController + func makeUIViewController(context: Context) -> MapViewController { + let mapVC = MapViewController() + mapVC.markerCallback = markerCallback + mapVC.mapLayer = layer + return mapVC } - func updateUIViewController(_ uiViewController: OurMaplyViewController, context: Context) { - context.coordinator.uiViewController = uiViewController - context.coordinator.link = link - + func updateUIViewController(_ uiViewController: MapViewController, context: Context) { // MARK: - Set map layer - context.coordinator.loader?.changeTileInfo(Utils.tileInfoFrom(layer: mapLayer)) - uiViewController.setZoomLimits(minZoom: mapLayer.minZoom, - maxZoom: mapLayer.maxZoom) + uiViewController.setMapLayer(layer) // MARK: - Set markers uiViewController.display(markers: markers, - isReport: false) - } - - static func dismantleUIViewController(_ uiViewController: MaplyViewController, coordinator: Coordinator) { - coordinator.loader?.shutdown() - uiViewController.teardown() - } -} - -class OurMaplyViewController: MaplyViewController { - enum Action { - case zoomIn - case zoomOut - } - - private var objects = [MaplyComponentObject]() - private var geofenceObjects = [MaplyComponentObject]() - - func action(_ action: Action) { - DispatchQueue.main.async { - switch action { - case .zoomIn: - self.zoomIn() - case .zoomOut: - self.zoomOut() - } - } - } - - func focusOn(point: MaplyCoordinate, - height: Float = 0.0000264, - animated: Bool = true) { - let z = max(height, getMinZoom()) - if animated { - animate(toPosition: point, height: z, time: 0.2) - } else { - setPosition(point, height: z) - } - } - - func zoomIn() { - let pos = getPosition() - let zoom = currentMapScale() / 2 - focusOn(point: pos, height: height(forMapScale: zoom)) - } - - func zoomOut() { - let pos = getPosition() - let zoom = currentMapScale() * 2 - focusOn(point: pos, height: height(forMapScale: zoom)) - } - - func clear(geofences: Bool = false) { - if geofences { - remove(geofenceObjects) - geofenceObjects.removeAll() - } else { - remove(objects) - objects.removeAll() - } - } - - func display(markers: [Marker], - isReport: Bool, - center: Bool = false) { - clear() - - let points = markers.map { marker in - MaplyCoordinateMakeWithDegrees(Float(marker.longitude), - Float(marker.latitude)) - } - - let fontSize = 11.0 - let colorReport = Color.green - let colorLabel = Color.secondary - let colorLabelOutline = Color.systemBackground - - let vectorDesc: [AnyHashable : Any] = [ - kMaplyColor: colorReport, - kMaplyVecWidth: 20.0 - ] - - let labelDesc: [AnyHashable : Any] = [ - kMaplyFont: UIFont.boldSystemFont(ofSize: fontSize), - kMaplyTextColor: colorLabel, - kMaplyTextOutlineColor: colorLabelOutline, - kMaplyTextOutlineSize: 3.0 - ] - - /* MARK: - Draw markers for positions */ - let screenMarkers = markers.enumerated().map { (i, marker) -> MaplyScreenMarker in - let screenMarker = MaplyScreenMarker() - screenMarker.layoutImportance = .greatestFiniteMagnitude - screenMarker.loc = MaplyCoordinateMakeWithDegrees(Float(marker.longitude), - Float(marker.latitude)) - var type: Marker.Type_ = .default_ - if isReport { - // For reports, position, start and end icons must be different - switch i { - case markers.startIndex: type = .reportStart - case markers.endIndex: type = .reportEnd - default: type = .reportPosition - } - } else { - type = marker.type - } - screenMarker.image = getIcon(markerType: type) - - var size = 50.0 - if isReport { - // For reports, position, start and end sizes must be different - switch i { - case markers.startIndex: size = 50.0 - case markers.endIndex: size = 50.0 - default: size = 25.0 - } - } - screenMarker.size = CGSize(width: size, height: size) - screenMarker.userObject = marker.id - screenMarker.selectable = true - - return screenMarker - } - - if let objs = addScreenMarkers(screenMarkers, desc: nil, mode: .any) { - objects.append(objs) - } - - /* MARK: - Add labels for markers */ - if !isReport && !markers.isEmpty { - let screenLabels = markers.map { marker -> MaplyScreenLabel in - let label = MaplyScreenLabel() - label.layoutImportance = .greatestFiniteMagnitude - var text = marker.name - if marker.name.count >= 20 { - let end = marker.name.index(marker.name.startIndex, offsetBy: 20) - text = String(marker.name[.. UIImage { - return UIImage(named: MarkerTransformations - .markerTypeToImageName(markerType: markerType))! - } -} - -// Source: https://stackoverflow.com/questions/65923718 -class BaseMapLink: ObservableObject { - @Published var action: OurMaplyViewController.Action? - - func zoomIn() { - action = .zoomIn - } - - func zoomOut() { - action = .zoomOut + isReport: false, + center: context.coordinator.shouldCenter) + context.coordinator.shouldCenter = false } } diff --git a/iosApp/iosApp/Map/MapViewController.xib b/iosApp/iosApp/Map/MapViewController.xib new file mode 100644 index 0000000..9d1694f --- /dev/null +++ b/iosApp/iosApp/Map/MapViewController.xib @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/iosApp/iosApp/Map/MapWrapperView.swift b/iosApp/iosApp/Map/MapWrapperView.swift deleted file mode 100644 index 7450eed..0000000 --- a/iosApp/iosApp/Map/MapWrapperView.swift +++ /dev/null @@ -1,87 +0,0 @@ -/** - * TrackerMap - * Copyright (C) 2021-2022 Iván Ávalos , Henoch Ojeda - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -import SwiftUI -import Combine -import WhirlyGlobeMaplyComponent -import shared - -struct MapWrapperView: View { - @Binding var layer: MapLayer - @Binding var markers: [Marker] - var markerCallback: ((Int32?) -> Void)? - @ObservedObject var link: BaseMapLink = BaseMapLink() - - var body: some View { - ZStack { - // MARK: - Map - MapView(mapLayer: $layer, - markers: $markers, - markerCallback: markerCallback, - link: link) - - // MARK: - Attribution - VStack { - HyperlinkText(html: layer.attribution) - .font(.footnote) - .lineLimit(3) - .foregroundColor(.label.opacity(0.35)) - .padding(6.0) - .background(.systemBackground.opacity(0.35)) - } - .frame(maxWidth: .infinity, - maxHeight: .infinity, - alignment: .bottom) - .allowsHitTesting(false) - - // MARK: - Controls - VStack { - // MARK: Zoom in - Button { - print ("Zoom in!") - link.zoomIn() - } label: { - Image(systemName: "plus").imageScale(.large) - } - .buttonStyle(ControlButtonStyle()) - - // MARK: Zoom out - Button { - print("Zoom out!") - link.zoomOut() - } label: { - Image(systemName: "minus").imageScale(.large) - } - .buttonStyle(ControlButtonStyle()) - } - .frame(maxWidth: .infinity, - maxHeight: .infinity, - alignment: .topTrailing) - .padding() - } - } -} - -struct ControlButtonStyle: ButtonStyle { - func makeBody(configuration: Configuration) -> some View { - configuration.label - .frame(width: 50, height: 50) - .foregroundColor(configuration.isPressed ? .secondary : .primary) - .background(configuration.isPressed ? .secondarySystemBackground : .systemBackground) - .clipShape(Circle()) - } -} diff --git a/iosApp/iosApp/Map/UnitMapView.swift b/iosApp/iosApp/Map/UnitMapView.swift index e1f90a0..5f418f1 100644 --- a/iosApp/iosApp/Map/UnitMapView.swift +++ b/iosApp/iosApp/Map/UnitMapView.swift @@ -23,9 +23,9 @@ struct UnitMapView: View { var body: some View { ZStack { - MapWrapperView(layer: $unitsViewModel.mapLayerType, - markers: $unitsViewModel.markers, - markerCallback: unitsViewModel.selectUnitWith) + MapView(layer: $unitsViewModel.mapLayerType, + markers: $unitsViewModel.markers, + markerCallback: unitsViewModel.selectUnitWith) if let unit = unitsViewModel.selectedUnit { VStack { DeviceRow(unit: unit, callback: { action in diff --git a/iosApp/iosApp/Shared/HyperlinkText.swift b/iosApp/iosApp/Shared/HyperlinkText.swift index 69c758b..a735f8d 100644 --- a/iosApp/iosApp/Shared/HyperlinkText.swift +++ b/iosApp/iosApp/Shared/HyperlinkText.swift @@ -15,129 +15,89 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -import SwiftUI -import SwiftUIFlowLayout +import Foundation +import UIKit -// Source: https://swiftuirecipes.com/blog/hyperlinks-in-swiftui-text -struct HyperlinkText: View { - private let pairs: [StringWithAttributes] - - init(_ attributedString: NSAttributedString) { - pairs = attributedString.stringsWithAttributes +// Source: https://stackoverflow.com/a/50272137 +extension UIColor { + enum HexFormat { + case RGB + case ARGB + case RGBA + case RRGGBB + case AARRGGBB + case RRGGBBAA } - - init?(html: String) { - if let data = html.data(using: .utf8), - let attributedString = try? NSAttributedString(data: data, - options: [.documentType: NSAttributedString.DocumentType.html], - documentAttributes: nil) { - self.init(attributedString) - } else { - return nil - } - } - - var body: some View { - FlowLayout(mode: .vstack, - binding: .constant(false), - items: pairs, - itemSpacing: 0) { pair in - if let link = pair.attrs[.link], - let url = link as? URL { - Text(pair) - .onTapGesture { - if UIApplication.shared.canOpenURL(url) { - UIApplication.shared.open(url) - } - } - } else { - Text(pair) - } - } - } -} -struct StringWithAttributes: Hashable, Identifiable { - let id = UUID() - let string: String - let attrs: [NSAttributedString.Key: Any] - - static func == (lhs: StringWithAttributes, rhs: StringWithAttributes) -> Bool { - lhs.id == rhs.id - } - - func hash(into hasher: inout Hasher) { - hasher.combine(id) + enum HexDigits { + case d3, d4, d6, d8 } -} -extension NSAttributedString { - var stringsWithAttributes: [StringWithAttributes] { - var attributes = [StringWithAttributes]() - enumerateAttributes(in: NSRange(location: 0, length: length), options: []) { (attrs, range, _) in - let string = attributedSubstring(from: range).string - attributes.append(StringWithAttributes(string: string, attrs: attrs)) - } - return attributes - } -} + func hexString(_ format: HexFormat = .RRGGBBAA) -> String { + let maxi = [.RGB, .ARGB, .RGBA].contains(format) ? 16 : 256 -extension Text { - init(_ singleAttribute: StringWithAttributes) { - let string = singleAttribute.string - let attrs = singleAttribute.attrs - var text = Text(string) - - /*if let font = attrs[.font] as? UIFont { - text = text.font(.init(font)) - } - - if let color = attrs[.foregroundColor] as? UIColor { - text = text.foregroundColor(Color(color)) - }*/ - - if let kern = attrs[.kern] as? CGFloat { - text = text.kerning(kern) - } - - if #available(iOS 14.0, *) { - if let tracking = attrs[.tracking] as? CGFloat { - text = text.tracking(tracking) - } + func toI(_ f: CGFloat) -> Int { + return min(maxi - 1, Int(CGFloat(maxi) * f)) } - - if let strikethroughStyle = attrs[.strikethroughStyle] as? NSNumber, strikethroughStyle != 0 { - if let strikethroughColor = (attrs[.strikethroughColor] as? UIColor) { - text = text.strikethrough(true, color: Color(strikethroughColor)) - } else { - text = text.strikethrough(true) - } - } - - if let underlineStyle = attrs[.underlineStyle] as? NSNumber, - underlineStyle != 0 { - if let underlineColor = (attrs[.underlineColor] as? UIColor) { - text = text.underline(true, color: Color(underlineColor)) - } else { - text = text.underline(true) - } - } - - if let baselineOffset = attrs[.baselineOffset] as? NSNumber { - text = text.baselineOffset(CGFloat(baselineOffset.floatValue)) + + var r: CGFloat = 0 + var g: CGFloat = 0 + var b: CGFloat = 0 + var a: CGFloat = 0 + + self.getRed(&r, green: &g, blue: &b, alpha: &a) + + let ri = toI(r) + let gi = toI(g) + let bi = toI(b) + let ai = toI(a) + + switch format { + case .RGB: return String(format: "#%X%X%X", ri, gi, bi) + case .ARGB: return String(format: "#%X%X%X%X", ai, ri, gi, bi) + case .RGBA: return String(format: "#%X%X%X%X", ri, gi, bi, ai) + case .RRGGBB: return String(format: "#%02X%02X%02X", ri, gi, bi) + case .AARRGGBB: return String(format: "#%02X%02X%02X%02X", ai, ri, gi, bi) + case .RRGGBBAA: return String(format: "#%02X%02X%02X%02X", ri, gi, bi, ai) } - - self = text } - - init(_ attributes: [StringWithAttributes]) { - self.init("") - for singleAttribute in attributes { - self = self + Text(singleAttribute) + + func hexString(_ digits: HexDigits) -> String { + switch digits { + case .d3: return hexString(.RGB) + case .d4: return hexString(.RGBA) + case .d6: return hexString(.RRGGBB) + case .d8: return hexString(.RRGGBBAA) } } - - init(_ attributedString: NSAttributedString) { - self.init(attributedString.stringsWithAttributes) +} + +// Source: https://swiftuirecipes.com/blog/swiftui-text-with-html-via-nsattributedstring +class HtmlString { + static func htmlToAttrStr(_ html: String, size: CGFloat, color: UIColor) -> NSAttributedString? { + let fullHTML = """ + + + + + + + \(html) + + + """ + if let data = fullHTML.data(using: .utf8) { + return try? NSAttributedString( + data: data, + options: [.documentType: NSAttributedString.DocumentType.html], + documentAttributes: nil) + } + return nil } } -- cgit v1.2.3