diff options
Diffstat (limited to 'iosApp/iosApp')
-rw-r--r-- | iosApp/iosApp/Map/MapView.swift | 307 | ||||
-rw-r--r-- | iosApp/iosApp/Map/MapViewController.xib | 104 | ||||
-rw-r--r-- | iosApp/iosApp/Map/MapWrapperView.swift | 87 | ||||
-rw-r--r-- | iosApp/iosApp/Map/UnitMapView.swift | 6 | ||||
-rw-r--r-- | iosApp/iosApp/Shared/HyperlinkText.swift | 186 |
5 files changed, 196 insertions, 494 deletions
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[..<end]) - } - label.text = text - label.loc = MaplyCoordinateMakeWithDegrees(Float(marker.longitude), - Float(marker.latitude)) - label.offset = CGPoint(x: 0.0, y: 25.0) - - return label - } - - if let objs = addScreenLabels(screenLabels, desc: labelDesc) { - objects.append(objs) - } - } - - /* MARK: - Draw polyline for report */ - if isReport && !markers.isEmpty { - let geoJSON: [AnyHashable : Any] = [ - "type": "FeatureCollection", - "features": [ - [ - "type": "LineString", - "coordinates": points.map({ point in - [point.x, point.y] - }) - ] - ] - ] - if let vector = MaplyVectorObject(fromGeoJSONDictionary: geoJSON) { - if let objs = addVectors([vector], desc: vectorDesc, mode: .any) { - objects.append(objs) - } - } - } - - /* MARK: - Center map to bounds */ - if center && !markers.isEmpty { - let box = MaplyBoundingBoxExpandByFraction( - MaplyBoundingBoxFromCoordinates(points, UInt32(points.count)), 0.1) - let center = MaplyCoordinate(x: (box.ur.x + box.ll.x) / 2, - y: (box.ur.y + box.ll.y) / 2) - let zoom = max(findHeight(toViewBounds: box, pos: center), getMinZoom()) - setPosition(center, height: zoom) - } - } - - func setZoomLimits(minZoom: Int32, maxZoom: Int32) { - setZoomLimitsMin( - height(forMapScale: Float(truncating: - MapCalculus.companion.zoomLevelToScale(zoom: maxZoom) - ?? MapCalculus.companion.zoomLevelToScale(zoom: 21)! - )), - max: height(forMapScale: Float(truncating: - MapCalculus.companion.zoomLevelToScale(zoom: minZoom) - ?? MapCalculus.companion.zoomLevelToScale(zoom: 1)! - ))) - } - - private func getIcon(markerType: Marker.Type_) -> 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 @@ +<?xml version="1.0" encoding="UTF-8"?> +<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="19529" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES"> + <device id="retina5_9" orientation="portrait" appearance="light"/> + <dependencies> + <deployment identifier="iOS"/> + <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="19519"/> + <capability name="Image references" minToolsVersion="12.0"/> + <capability name="Safe area layout guides" minToolsVersion="9.0"/> + <capability name="System colors in document resources" minToolsVersion="11.0"/> + <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/> + </dependencies> + <objects> + <placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner" customClass="MapViewController" customModule="iosApp" customModuleProvider="target"> + <connections> + <outlet property="attributionText" destination="R7q-da-DO1" id="Noy-FR-ZRo"/> + <outlet property="mapContainer" destination="T6C-8E-ftt" id="esY-iz-qsl"/> + <outlet property="view" destination="i5M-Pr-FkT" id="sfx-zR-JGt"/> + </connections> + </placeholder> + <placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/> + <view clearsContextBeforeDrawing="NO" contentMode="scaleToFill" id="i5M-Pr-FkT"> + <rect key="frame" x="0.0" y="0.0" width="375" height="812"/> + <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/> + <subviews> + <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="XUh-lD-GKm"> + <rect key="frame" x="317" y="52" width="50" height="50"/> + <color key="backgroundColor" systemColor="systemBackgroundColor"/> + <constraints> + <constraint firstAttribute="height" constant="50" id="SFA-XB-o3z"/> + <constraint firstAttribute="width" constant="50" id="ezA-Bu-6Mq"/> + </constraints> + <inset key="imageEdgeInsets" minX="0.0" minY="0.0" maxX="2.2250738585072014e-308" maxY="0.0"/> + <state key="normal"> + <color key="titleColor" systemColor="labelColor"/> + <imageReference key="image" image="plus" catalog="system" symbolScale="large"/> + <preferredSymbolConfiguration key="preferredSymbolConfiguration"/> + </state> + <connections> + <action selector="onZoomInPressed:" destination="-1" eventType="touchUpInside" id="x4P-sk-jdi"/> + </connections> + </button> + <button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="kdu-l3-IK7"> + <rect key="frame" x="317" y="110" width="50" height="50"/> + <color key="backgroundColor" systemColor="systemBackgroundColor"/> + <constraints> + <constraint firstAttribute="height" relation="greaterThanOrEqual" constant="50" id="2yG-8e-OLn"/> + <constraint firstAttribute="width" relation="greaterThanOrEqual" constant="50" id="ACq-Cq-PTq"/> + <constraint firstAttribute="width" constant="50" id="Ep3-3d-iue"/> + <constraint firstAttribute="height" constant="50" id="gMl-lS-Lvw"/> + </constraints> + <inset key="imageEdgeInsets" minX="0.0" minY="0.0" maxX="2.2250738585072014e-308" maxY="0.0"/> + <state key="normal"> + <color key="titleColor" systemColor="labelColor"/> + <imageReference key="image" image="minus" catalog="system" symbolScale="large"/> + <preferredSymbolConfiguration key="preferredSymbolConfiguration"/> + </state> + <connections> + <action selector="onZoomOutPressed:" destination="-1" eventType="touchUpInside" id="rt2-fy-VCy"/> + </connections> + </button> + <textView clipsSubviews="YES" multipleTouchEnabled="YES" userInteractionEnabled="NO" alpha="0.40000000000000002" contentMode="scaleToFill" scrollEnabled="NO" editable="NO" text="Copyright (C) 2020 OpenStreetMap" selectable="NO" translatesAutoresizingMaskIntoConstraints="NO" id="R7q-da-DO1"> + <rect key="frame" x="0.0" y="745" width="375" height="33"/> + <color key="backgroundColor" systemColor="systemBackgroundColor"/> + <color key="textColor" systemColor="labelColor"/> + <fontDescription key="fontDescription" name=".AppleSystemUIFont" family=".AppleSystemUIFont" pointSize="14"/> + <textInputTraits key="textInputTraits" autocapitalizationType="sentences"/> + </textView> + <view contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="T6C-8E-ftt"> + <rect key="frame" x="0.0" y="44" width="375" height="734"/> + <color key="backgroundColor" systemColor="secondarySystemBackgroundColor"/> + </view> + </subviews> + <viewLayoutGuide key="safeArea" id="fnl-2z-Ty3"/> + <color key="backgroundColor" systemColor="systemBackgroundColor"/> + <constraints> + <constraint firstItem="T6C-8E-ftt" firstAttribute="trailing" secondItem="fnl-2z-Ty3" secondAttribute="trailing" id="Adi-2F-rXH"/> + <constraint firstItem="T6C-8E-ftt" firstAttribute="top" secondItem="fnl-2z-Ty3" secondAttribute="top" id="EGw-gK-1tb"/> + <constraint firstItem="fnl-2z-Ty3" firstAttribute="trailing" secondItem="XUh-lD-GKm" secondAttribute="trailing" constant="8" id="END-FB-FNk"/> + <constraint firstItem="T6C-8E-ftt" firstAttribute="leading" secondItem="fnl-2z-Ty3" secondAttribute="leading" id="HzR-cl-3zE"/> + <constraint firstItem="R7q-da-DO1" firstAttribute="bottom" secondItem="fnl-2z-Ty3" secondAttribute="bottom" id="NMu-yb-3sE"/> + <constraint firstItem="fnl-2z-Ty3" firstAttribute="trailing" secondItem="kdu-l3-IK7" secondAttribute="trailing" constant="8" id="Um2-vS-uoQ"/> + <constraint firstItem="R7q-da-DO1" firstAttribute="leading" secondItem="fnl-2z-Ty3" secondAttribute="leading" id="XM1-Gu-sfE"/> + <constraint firstItem="kdu-l3-IK7" firstAttribute="top" secondItem="XUh-lD-GKm" secondAttribute="bottom" constant="8" id="hTB-Wm-3eQ"/> + <constraint firstItem="R7q-da-DO1" firstAttribute="trailing" secondItem="fnl-2z-Ty3" secondAttribute="trailing" id="heK-zH-Ob4"/> + <constraint firstItem="T6C-8E-ftt" firstAttribute="bottom" secondItem="fnl-2z-Ty3" secondAttribute="bottom" id="oT6-iC-7PW"/> + <constraint firstItem="XUh-lD-GKm" firstAttribute="top" secondItem="fnl-2z-Ty3" secondAttribute="top" constant="8" id="oXT-Ln-tOw"/> + </constraints> + <point key="canvasLocation" x="140" y="96.059113300492612"/> + </view> + </objects> + <resources> + <image name="minus" catalog="system" width="128" height="24"/> + <image name="plus" catalog="system" width="128" height="113"/> + <systemColor name="labelColor"> + <color white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> + </systemColor> + <systemColor name="secondarySystemBackgroundColor"> + <color red="0.94901960784313721" green="0.94901960784313721" blue="0.96862745098039216" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> + </systemColor> + <systemColor name="systemBackgroundColor"> + <color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/> + </systemColor> + </resources> +</document> 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 <avalos@disroot.org>, Henoch Ojeda <imhenoch@protonmail.com> - * - * 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 <http://www.gnu.org/licenses/>. - */ -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 <http://www.gnu.org/licenses/>. */ -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 = """ + <!doctype html> + <html> + <head> + <style> + body { + font-family: -apple-system; + font-size: \(size)px; + color: \(color.hexString(.RGB)) + } + </style> + </head> + <body> + \(html) + </body> + </html> + """ + if let data = fullHTML.data(using: .utf8) { + return try? NSAttributedString( + data: data, + options: [.documentType: NSAttributedString.DocumentType.html], + documentAttributes: nil) + } + return nil } } |