Set range of colored text for UISegmentedControl - uisegmentedcontrol

According to the following documentation (https://developer.apple.com/documentation/uikit/uisegmentedcontrol/1618570-settitletextattributes)
I should be able to add attributes to change how it looks for a particular mode.
modalitySegmentedControl.setTitle("LDR ("+(stateController?.tdfvariables.selectedRadionuclide.name ?? "-") + ")", forSegmentAt: Constants.LDRButton)
let colorAttribute = [ NSAttributedString.Key.foregroundColor: UIColor.systemTeal ]
modalitySegmentedControl.setTitleTextAttributes(colorAttribute, for: .selected)
In short the text on the control is basically "LDR (I-125)". Currently this code highlights the entire selection teal. I'm looking for a way to only highlight the (I-125) only with a teal color. I can do this with regular UILabels by defining a range that the attributes act upon, but I can't seem to find a way to set a specific color range with the UISegmentedControl?
Is this possible to do?
It currently looks like this:
I want the LDR to be white color and only teal on the (I-125) part.

In short I think it's not possible. Check my hacky playground:
//: A UIKit based Playground for presenting user interface
import UIKit
import PlaygroundSupport
extension UIView {
class func getAllSubviews<T: UIView>(from parentView: UIView) -> [T] {
return parentView.subviews.flatMap { subView -> [T] in
var result = getAllSubviews(from: subView) as [T]
if let view = subView as? T { result.append(view) }
return result
}
}
class func getAllSubviews(from parentView: UIView, types: [UIView.Type]) -> [UIView] {
return parentView.subviews.flatMap { subView -> [UIView] in
var result = getAllSubviews(from: subView) as [UIView]
for type in types {
if subView.classForCoder == type {
result.append(subView)
return result
}
}
return result
}
}
func getAllSubviews<T: UIView>() -> [T] { return UIView.getAllSubviews(from: self) as [T] }
func get<T: UIView>(all type: T.Type) -> [T] { return UIView.getAllSubviews(from: self) as [T] }
func get(all types: [UIView.Type]) -> [UIView] { return UIView.getAllSubviews(from: self, types: types) }
}
class MyViewController : UIViewController {
var myString: String = "LDR (I-125)"
var myString42: String = "424242424242"
var attributedString = NSMutableAttributedString()
override func loadView() {
let view = UIView()
view.backgroundColor = .white
let items = ["EBRT", "LDR (I-125)", "PERM"]
let modalitySegmentedControl = UISegmentedControl(items: items)
modalitySegmentedControl.frame = CGRect(x: 20, y: 200, width: 300, height: 20)
modalitySegmentedControl.backgroundColor = .white
attributedString = NSMutableAttributedString(string: myString, attributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 18)])
attributedString.addAttribute(NSAttributedString.Key.foregroundColor, value: UIColor.red, range: NSRange(location:4, length:7))
let subviews = modalitySegmentedControl.getAllSubviews()
for view in subviews {
if view is UILabel {
if let label = view as? UILabel, label.text == myString {
print(label.attributedText)
label.attributedText = attributedString
//label.text = "42" // this works
print(label.attributedText) // looks changed
}
}
}
let subviews2 = modalitySegmentedControl.getAllSubviews()
for view in subviews2 {
if view is UILabel {
if let label = view as? UILabel, label.text == myString {
print(label.attributedText) // but it didn't change
}
}
}
let lab = UILabel()
lab.frame = CGRect(x: 40, y: 250, width: 300, height: 20)
lab.attributedText = attributedString
view.addSubview(lab)
view.addSubview(modalitySegmentedControl)
self.view = view
}
}
// Present the view controller in the Live View window
PlaygroundPage.current.liveView = MyViewController()
You can find specific UILabel subview of UISegmentedControl and even can change the text, but attribute changes doesn't work.
Related question: Segmented Control set attributed title in each segment

Related

Is this the proper way to use PHPicker in SwiftUI? Because I'm getting a lot of leaks

I am trying to figure out if my code is causing the problem or if I should submit a bug report to Apple.
In a new project, I have this code:
ContentView()
import SwiftUI
struct ContentView: View {
#State private var showingImagePicker = false
#State private var inputImage: UIImage?
#State private var image: Image?
var body: some View {
ZStack {
Rectangle()
.fill(Color.secondary)
if image != nil {
image?
.resizable()
.scaledToFit()
} else {
Text("Tap to select a picture")
.foregroundColor(.white)
.font(.headline)
}
}
.onTapGesture {
self.showingImagePicker = true
}
.sheet(isPresented: $showingImagePicker, onDismiss: loadImage){
SystemImagePicker(image: self.$inputImage)
}
}
func loadImage() {
guard let inputImage = inputImage else { return }
image = Image(uiImage: inputImage)
}
}
SystemImagePicker.swift
import SwiftUI
struct SystemImagePicker: UIViewControllerRepresentable {
#Environment(\.presentationMode) private var presentationMode
#Binding var image: UIImage?
func makeUIViewController(context: Context) -> PHPickerViewController {
var configuration = PHPickerConfiguration()
configuration.selectionLimit = 1
configuration.filter = .images
let picker = PHPickerViewController(configuration: configuration)
picker.delegate = context.coordinator
return picker
}
func updateUIViewController(_ uiViewController: PHPickerViewController, context: Context) {
}
func makeCoordinator() -> Coordinator {
return Coordinator(parent: self)
}
class Coordinator: NSObject, PHPickerViewControllerDelegate {
let parent: SystemImagePicker
init(parent: SystemImagePicker) {
self.parent = parent
}
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
for img in results {
guard img.itemProvider.canLoadObject(ofClass: UIImage.self) else { return }
img.itemProvider.loadObject(ofClass: UIImage.self) { image, error in
if let error = error {
print(error)
return
}
guard let image = image as? UIImage else { return }
self.parent.image = image
self.parent.presentationMode.wrappedValue.dismiss()
}
}
}
}
}
But when selecting just one image (as per my code, not selecting and then "changing my mind" and selecting another, different image), I get these leaks when running the memory graph in Xcode.
Is it my code, or is this on Apple?
For what it is worth, the Cancel button on the imagepicker doesn't work either. So, the user cannot just close the picker sheet, an image MUST be selected to dismiss the sheet.
Further note on old UIImagePickerController
Previously, I've used this code for the old UIImagePickerController
import SwiftUI
struct ImagePicker: UIViewControllerRepresentable {
#Environment(\.presentationMode) var presentationMode
#Binding var image: UIImage?
class Coordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate {
let parent: ImagePicker
init(_ parent: ImagePicker) {
self.parent = parent
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
if let uiImage = info[.originalImage] as? UIImage {
parent.image = uiImage
}
parent.presentationMode.wrappedValue.dismiss()
}
deinit {
print("deinit")
}
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIViewController(context: UIViewControllerRepresentableContext<ImagePicker>) -> UIImagePickerController {
let picker = UIImagePickerController()
picker.delegate = context.coordinator
return picker
}
func updateUIViewController(_ uiViewController: UIImagePickerController, context: UIViewControllerRepresentableContext<ImagePicker>) {
}
}
This also result in leaks from choosing an image, but far fewer of them:
I know it's been over a year since you asked this question but hopefully this helps you or someone else looking for the answer.
I used this code in a helper file:
import SwiftUI
import PhotosUI
struct ImagePicker: UIViewControllerRepresentable {
let configuration: PHPickerConfiguration
#Binding var selectedImage: UIImage?
#Binding var showImagePicker: Bool
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeUIViewController(context: Context) -> PHPickerViewController {
let picker = PHPickerViewController(configuration: configuration)
picker.delegate = context.coordinator
return picker
}
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
}
}
extension ImagePicker {
class Coordinator: NSObject, PHPickerViewControllerDelegate {
private let parent: ImagePicker
init(_ parent: ImagePicker) {
self.parent = parent
}
func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
picker.dismiss(animated: true) {
self.parent.showImagePicker = false
}
guard let provider = results.first?.itemProvider else { return }
if provider.canLoadObject(ofClass: UIImage.self) {
provider.loadObject(ofClass: UIImage.self) { image, _ in
self.parent.selectedImage = image as? UIImage
}
}
parent.showImagePicker = false
}
}
}
This goes in your view (I set up configuration here so I could pass in custom versions depending on what I'm using the picker for, 2 are provided):
#State private var showImagePicker = false
#State private var selectedImage: UIImage?
#State private var profileImage: Image?
var profileConfig: PHPickerConfiguration {
var config = PHPickerConfiguration()
config.filter = .images
config.selectionLimit = 1
config.preferredAssetRepresentationMode = .current
return config
}
var mediaConfig: PHPickerConfiguration {
var config = PHPickerConfiguration()
config.filter = .any(of: [.images, .videos])
config.selectionLimit = 1
config.preferredAssetRepresentationMode = .current
return config
}
This goes in your body. You can customize it how you want but this is what I have so I didn't want to try and piece it out:
HStack {
Button {
showImagePicker.toggle()
} label: {
Text("Select Photo")
.foregroundColor(Color("AccentColor"))
}
.sheet(isPresented: $showImagePicker) {
loadImage()
} content: {
ImagePicker(configuration: profileConfig, selectedImage: $selectedImage, showImagePicker: $showImagePicker)
}
}
if profileImage != nil {
profileImage?
.resizable()
.scaledToFill()
.frame(width: 100, height: 100)
.clipShape(Circle())
.shadow(radius: 5)
.overlay(Circle().stroke(Color.black, lineWidth: 2))
}
else {
Image(systemName: "person.crop.circle")
.resizable()
.foregroundColor(Color("AccentColor"))
.frame(width: 100, height: 100)
}
I will also give you the func for loading the image (I will be resamp:
func loadImage() {
guard let selectedImage = selectedImage else { return }
profileImage = Image(uiImage: selectedImage)
}
I also used this on my Form to update the image if it is changed but you can use it on whatever you're using for your body (List, Form, etc. Whatever takes .onChange):
.onChange(of: selectedImage) { _ in
loadImage()
}
I noticed in a lot of tutorials there is little to no mention of this line which is what makes the cancel button function (I don't know if the closure is necessary but I added it and it worked so I left it in the example):
picker.dismiss(animated: true)
I hope I added everything to help you. It doesn't appear to leak anything and gives you use of the cancel button.
Good luck!

SwiftUI UITextView wrapper how to size it content to match parent

I have such UITextView wrapper and it works but when I place it inside List row. I would like it to autosize, i.e. have width that match parent in this case List Row VStack { }, and height that is autosize based on text length. Text should wrap.
Now it nearly works but the longer text like a URL goes outside available row width.
List {
VStack(alignment: .leading) {
if self.hasNote {
TextView(text: text)
}
}
struct TextView: UIViewRepresentable {
// MARK: - Properties
let text: String
init(text: String) {
self.text = text
}
func makeUIView(context: Context) -> UITextView {
let textView = UITextView()
//textView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
textView.backgroundColor = UIColor.clear
textView.isScrollEnabled = false
textView.attributedText = self.attributedText
textView.textContainer.lineBreakMode = .byWordWrapping
//textView.layoutIfNeeded()
textView.sizeToFit()
return textView
}
}
UPDATE
Now I have something like this it does fit parent SwiftUI views (rows in List) but instead it does not wrap text (or stretch vertically based on content size. If scrolled it wraps text correctly.
Also if I set .frame(height: 500) I can see it wraps text. But it doesn't autosize correctly.
func makeUIView(context: Context) -> UITextView {
let textView = UITextView()
textView.backgroundColor = UIColor.clear
textView.isScrollEnabled = false
textView.isSelectable = true
let linkAttrs : [NSAttributedString.Key : Any] = [
.foregroundColor: accentColor,
.underlineColor: accentColor,
.underlineStyle: NSUnderlineStyle.single.rawValue
]
textView.linkTextAttributes = linkAttrs
textView.attributedText = self.attributedText
textView.textContainer.lineBreakMode = .byWordWrapping
textView.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
textView.setContentCompressionResistancePriority(.defaultHigh, for: .vertical)
textView.setContentHuggingPriority(.defaultHigh, for: .horizontal)
textView.setContentHuggingPriority(.defaultLow, for: .vertical)
textView.sizeToFit()
return textView
}

SwiftUI: make layout relative to a central view

Assume I build a view like this:
struct MyView: View {
#State private var a: String
#State private var b: String
#State private var c: String
var body: some View {
VStack {
HStack {
Text(a)
// this is the central view
Text(b).font(.headline)
}
Text(c)
}
}
}
I would like the central text view (the one displaying b) to be the anchor of the layout. That is, no matter how other text values change, I would like the central text to always stay in the centre of MyView (the centre of the text element and the centre of MyView should stay identical) and the other text elements should be laid out around the central one.
How to I achieve this? I tried to look at alignment guides, but I just don't seem to understand how to use them properly.
After spending some time to learn how alignment works in detail, I managed to arrive at a solution that only uses stacks and custom alignments, with minimal alignment guides and without needing to save any intermediate state. It's purely declarative, so I am supposed this is how SwiftUI designers intended it. I still think that there might have been a better design for it, but one can work with it.
struct ContentView: View {
#State var a: String = "AAAAA"
#State var b: String = "BBBB"
#State var c: String = "CCCCCC"
var body: some View {
VStack {
ZStack(alignment: .mid) {
// create vertical and horizontal
// space to align to
HStack { Spacer() }
VStack { Spacer() }
VStack(alignment: .midX) {
Text(self.a)
HStack(alignment: .center) {
Text(self.c)
Text(self.b)
.font(.title)
.border(Color.blue)
.alignmentGuide(.midX) { d in
(d[.leading] + d[.trailing])/2
}
.alignmentGuide(.midY) { d in
(d[.top] + d[.bottom])/2
}
}
}
}
.layoutPriority(1.0)
.overlay(CrossHair().stroke(Color.pink, lineWidth: 2))
TextField("", text: self.$b).textFieldStyle(RoundedBorderTextFieldStyle())
}
}
}
fileprivate extension HorizontalAlignment {
enum MidX : AlignmentID {
static func defaultValue(in d: ViewDimensions) -> CGFloat {
return (d[.leading] + d[.trailing])/2
}
}
static let midX = HorizontalAlignment(MidX.self)
}
fileprivate extension VerticalAlignment {
enum MidY : AlignmentID {
static func defaultValue(in d: ViewDimensions) -> CGFloat {
return (d[.top] + d[.bottom])/2
}
}
static let midY = VerticalAlignment(MidY.self)
}
fileprivate extension Alignment {
static let mid = Alignment(horizontal: .midX, vertical: .midY)

Pan to new mapView Annotation Location - not zoom

When adding an annotation to my mapView using mapView.addAnnotation, mapView.setRegion correctly animates and displays the map's center & span.
But when adding a new annotation and then calling mapView.setRegion, the view again starts from very wide, and then animates/zooms in to the new center & span.
I would like to know if there is a way to PAN from the previous region to the new region (center & span), rather than starting zoomed out, then zooming all the way back in again.
mapView.setRegion starts zoom from way out here each time:
I have defined 2 custom classes : MarkAnnotation & MarkAnnotationView
class MarkAnnotation : NSObject, MKAnnotation {
var coordinate: CLLocationCoordinate2D
var title: String?
var subtitle: String?
init(coordinate: CLLocationCoordinate2D, title: String, subtitle: String) {
self.coordinate = coordinate
self.title = title
self.subtitle = subtitle
}
}
class MarkAnnotationView: MKAnnotationView {
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
}
override init(annotation: MKAnnotation?, reuseIdentifier: String?) {
super.init(annotation: annotation, reuseIdentifier: reuseIdentifier)
guard let markAnnotation = self.annotation as? MarkAnnotation else { return }
image = UIImage(named: "customAnnotation")
}
}
and in my MKMapViewDelegate :
func addMarkAnnotation() {
for mark in marks {
let markName : String = mark.name
let markLat : Double = mark.lat
let markLon : Double = mark.lon
let markLL = CLLocationCoordinate2DMake(markLat, markLon)
let markAnnotation = MarkAnnotation(coordinate:markLL, title:markNumStr, subtitle: markName)
mapView.addAnnotation(markAnnotation)
}
}
func mapView(_ mapView: MKMapView, viewFor annotation: MKAnnotation) -> MKAnnotationView? {
let annotationView = MarkAnnotationView(annotation: annotation, reuseIdentifier: "MarkView")
annotationView.canShowCallout = true
return annotationView
}
func centerMapOnLocation(location:CLLocation, span:Double) {
let mapSpan = MKCoordinateSpanMake(0.2, 0.2)
let recenterRegion = MKCoordinateRegion(center: location.coordinate, span: mapSpan)
mapView.setRegion(recenterRegion, animated: true)
}
And then in the ViewController, when user presses a button, a mark is added at a specified lat/lon
func addMarkBtnPressed() {
// .... (have not shown : conversion of text fields from String to Double, adding Mark to marks array, etc ) ... All this works fine
addMarkAnnotation()
let mapCenter = CLLocation(latitude: markLat, longitude: markLon)
centerMapOnLocation(location: mapCenter, span: span)
}
Try the following code:
func centerMapOnLocation(location:CLLocation, span:Double) {
let mapSpan = mapView.region.span
let recenterRegion = MKCoordinateRegion(center: location.coordinate, span: mapSpan)
mapView.setRegion(recenterRegion, animated: true)
}

How do I detect Tap and Hold on a pin in MKMapView?

The title says it all. I'm trying to detect a tap on a pin in a MKMapView and I don't even know where to begin. Its not an UIView, so I can't add a gesture recognizer and I can't find a UIView in MKPlaceMark to add it.
You're question is not very clear but you can do like this:
import UIKit
import MapKit
protocol HandleMapSearch: class {
func dropPinZoomIn(placemark:MKPlacemark)
}
class ViewController: UIViewController {
func getDirections(){
// Here you can put anythings like:
guard let selectedPin = selectedPin else { return }
let mapItem = MKMapItem(placemark: selectedPin)
let launchOptions = [MKLaunchOptionsDirectionsModeKey: MKLaunchOptionsDirectionsModeDriving]
mapItem.openInMapsWithLaunchOptions(launchOptions)
}
}
extension ViewController : MKMapViewDelegate {
func mapView(mapView: MKMapView, viewForAnnotation annotation: MKAnnotation) -> MKAnnotationView?{
guard !(annotation is MKUserLocation) else { return nil }
let reuseId = "pin"
var pinView = mapView.dequeueReusableAnnotationViewWithIdentifier(reuseId) as? MKPinAnnotationView
if pinView == nil {
pinView = MKPinAnnotationView(annotation: annotation, reuseIdentifier: reuseId)
}
pinView?.pinTintColor = UIColor.orangeColor() // The pin's color
pinView?.canShowCallout = true // To set dialogue bubbles of the pin.
let smallSquare = CGSize(width: 30, height: 30)
let button = UIButton(frame: CGRect(origin: CGPointZero, size: smallSquare)) // To initialize the button in the dialogue bubbles of the pin.
button.addTarget(self, action: #selector(ViewController.getDirections), forControlEvents: .TouchUpInside) // To set and initialize the button.
pinView?.leftCalloutAccessoryView = button
return pinView
}
}
You can have more details in Thorn web site

Resources