I'm using the camera to capture images for an app. Once I touch the photo button, I am
only able to save the photo in a square format. I want to preserve the 4:3 format for
older cameras and eventually allow the user to choose 16:9 if desired. I have not been
able to find any code to programmatically chose aspect ratio. I only get square. I even
tried locking the Camera Mode in Camera Settings on the device. Thankfully, that
didn't work.
It is a SwiftUI app. In the CameraViewController below, if I remove the allowsEditing
option, the picker freezes - neither the Use Photo, nor Retake buttons function. The
app does not crash, just the view is frozen. I am able to swipe down the view to
dismiss and continue using the app.
I call the PhotoCaptureView below from a button in what would be the detail view of
a master/detail equivalent.
PhotoCaptureView:
import SwiftUI
struct PhotoCaptureView: View {
#EnvironmentObject var cameraSettings: CameraSettings
#Binding var photos: [UIImage]
var body: some View {
return VStack {
CameraViewController(photos: $photos)
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
}.edgesIgnoringSafeArea(.all)
}
}
And the model view CameraViewController:
import UIKit
import SwiftUI
import MobileCoreServices
import AVFoundation
struct CameraViewController: UIViewControllerRepresentable {
#EnvironmentObject var cameraSettings: CameraSettings
#Binding var photos: [UIImage]
func updateUIViewController(_ uiViewContyroller: UIImagePickerController, context: Context) {
//no code here
}
func makeUIViewController(context: UIViewControllerRepresentableContext<CameraViewController>) -> UIImagePickerController {
let vc = UIImagePickerController()
if UIImagePickerController.isSourceTypeAvailable(UIImagePickerController.SourceType.camera) {
vc.sourceType = .camera
vc.allowsEditing = true
vc.delegate = context.coordinator
return vc
}
return UIImagePickerController()
}
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, UINavigationControllerDelegate, UIImagePickerControllerDelegate {
var parent: CameraViewController
init(_ imagePickerController: CameraViewController) {
self.parent = imagePickerController
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
guard let image = info[.editedImage] as? UIImage else {
print("no image found")
return
}
print("Image taken successfully, with size \(image.size)")
parent.photos.append(image)
picker.dismiss(animated: true) {
print("in dismiss after successful picture")
}
}
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
picker.dismiss(animated: true) {
print("in did cancel")
}
}
private func pickerController(_ controller: UIImagePickerController, didSelect image: UIImage?) {
controller.dismiss(animated: true, completion: nil)
}
}
}
The CameraSettings are just Bools to determine whether to present the camera option.
class CameraSettings: ObservableObject {
#Published var isAuthorized: Bool = false
#Published var isCameraAvailable: Bool = false
}
Xcode 11.1 (11A1027) I am using an iPhone X iOS 13.1.2
Any guidance would be appreciated.
The info setting of didFinishPickingMediaWithInfo needs to specify "originalImage" to avoid the square only picture. Even with the edited version, however, it seems you can only move the image around - not change the aspect.
change the info line above to:
guard let image = info[.originalImage] as? UIImage else {
Then set:
vc.allowsEditing = false
Importantly the documentation tells me that: "The UIImagePickerController class supports portrait mode only."
Related
I have written an app to display timezones in a NSStatusBar popover. All good so far but when I add a second popover from a button inside the original popover view and include a TextField I start getting problems.
The TextField shows as having focus but it refuses input. If I toggle in and out of the second popover then I get a crash.
...as the first responder for window <_NSPopoverWindow: 0x7fb9c1807bb0>, but it is in a different window ((null))! This would eventually crash when the view is freed. The first responder will be set to nil.
I'm assuming these are related.
I have extracted just the NSStatus bar setup and the two popover views and it is fully repeatable in this toy instance. I use an EventMonitor to catch a click outside of the popover to close it. I don't think that is relevant but I have included it in the toy app for completeness
AppDelegate.swift
import Cocoa
import SwiftUI
#NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate {
var popover: NSPopover!
var statusBarItem: NSStatusItem!
var eventMonitor: EventMonitor?
var contentView = ContentView()
func applicationDidFinishLaunching(_ aNotification: Notification) {
// Create the popover
let popover = NSPopover()
popover.behavior = .transient
popover.contentViewController = NSHostingController(rootView: contentView)
self.popover = popover
// Create the status item
self.statusBarItem = NSStatusBar.system.statusItem(withLength: CGFloat(NSStatusItem.variableLength))
if let button = self.statusBarItem.button {
button.image = NSImage(named: "Icon")
button.action = #selector(togglePopover(_:))
}
eventMonitor = EventMonitor(mask: [.leftMouseDown, .rightMouseDown]) { [unowned self] event in
if self.popover.isShown {
closePopover(event)
}
}
eventMonitor?.start()
NSApp.activate(ignoringOtherApps: true)
}
#objc func togglePopover(_ sender: AnyObject?) {
if popover.isShown {
closePopover(sender)
} else {
showPopover(sender)
}
}
func showPopover(_ sender: AnyObject?) {
if let button = statusBarItem.button {
popover.show(relativeTo: button.bounds, of: button, preferredEdge: NSRectEdge.minY)
self.popover.contentViewController?.view.window?.becomeKey()
}
eventMonitor?.start()
}
func closePopover(_ sender: AnyObject?) {
popover.performClose(sender)
eventMonitor?.stop()
}
}
open class EventMonitor {
fileprivate var monitor: AnyObject?
fileprivate let mask: NSEvent.EventTypeMask
fileprivate let handler: (NSEvent?) -> ()
public init(mask: NSEvent.EventTypeMask, handler: #escaping (NSEvent?) -> ()) {
self.mask = mask
self.handler = handler
}
deinit {
stop()
}
open func start() {
monitor = NSEvent.addGlobalMonitorForEvents(matching: mask, handler: handler) as AnyObject?
}
open func stop() {
if monitor != nil {
NSEvent.removeMonitor(monitor!)
monitor = nil
}
}
}
ContentView.swift
import SwiftUI
struct ContentView: View {
#State private var showingPopover = false
#State var value: String = "Initial Value"
var body: some View {
VStack {
Button {
showingPopover = true
} label: {
Image(systemName: "globe")
.imageScale(.large)
.foregroundColor(.accentColor)
}
.popover(isPresented: $showingPopover) {
EditView(value: $value)
}
Text("Hello, world!")
}
.padding()
}
}
struct EditView: View {
#Binding var value: String
var body: some View {
VStack {
TextField("Location ", text: $value) // the location as a string
.multilineTextAlignment(.center)
.lineLimit(1)
.onSubmit {
print("value submit \(value)")
}
}
.padding()
.frame(width:200, height: 50)
}
}
I've searched for information and found references to similar problems with windowed applications and needing to click in the 'window' before clicking in the TextField but that doesn't seem to do anything in this context
I'm building on MacOS 12.6.2 with Target set for 12.0
I am trying to implement a feature in my app to get the color of the pixel or pixels in the center of the camera view without taking a photo, just based off of what the in-app camera view is being pointed at. My camera view is simple and posted below, but I cannot figure out how to access the data from the live preview or if it is even possible with this implementation.
import Foundation
import UIKit
import SwiftUI
struct CameraView: UIViewControllerRepresentable {
typealias UIViewControllerType = UIImagePickerController
func makeUIViewController(context: Context) -> UIViewControllerType {
let viewController = UIViewControllerType()
viewController.delegate = context.coordinator
viewController.sourceType = .camera
viewController.showsCameraControls = false
return viewController
}
func updateUIViewController(_ uiViewController: UIViewControllerType, context: Context) {
}
func makeCoordinator() -> CameraView.Coordinator {
return Coordinator(self)
}
}
extension CameraView {
class Coordinator : NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
var parent: CameraView
init(_ parent: CameraView) {
self.parent = parent
}
func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
print("Cancel pressed")
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
if let image = info[UIImagePickerController.InfoKey.originalImage] as? UIImage {
UIImageWriteToSavedPhotosAlbum(image, nil, nil, nil)
}
}
}
}
I'm struggling with a custom made RichTextField, i.e. a NSTextView in a NSViewRepresentable, which has the below code (programmed with the help of How to use an NSAttributedString with a ScrollView in SwiftUI?):
Setting the attributedString in the code works and I can change the formatting, but as soon as the application loses the focus, the RichTextField resets to the last value set programmatically:
Furthermore, when using the RichTextField in a List, the application goes into a loop.
RichTextField
import Foundation
import SwiftUI
struct RichTextField: NSViewRepresentable {
typealias NSViewType = NSTextView
#Binding var attributedString: NSMutableAttributedString
var isEditable: Bool
func makeNSView(context: Context) -> NSTextView {
let textView = NSTextView(frame: .zero)
textView.textStorage?.setAttributedString(self.attributedString)
textView.isEditable = isEditable
textView.translatesAutoresizingMaskIntoConstraints = false
textView.autoresizingMask = [.width, .height]
return textView
}
func updateNSView(_ nsView: NSTextView, context: Context) {
nsView.textStorage?.setAttributedString(self.attributedString)
}
}
View
import SwiftUI
struct EditWindow: View {
#ObservedObject var model: EditEntryViewModel
var body: some View {
VStack (alignment: .leading, spacing: 20) {
RichTextField(attributedString: self.$model.answer1, isEditable: true)
.frame(maxWidth: .infinity, maxHeight: .infinity)
Spacer()
}.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
}
}
ViewModel
import Foundation
import SwiftUI
class EditEntryViewModel: ObservableObject {
init(entryID: Int32) {
let entryToUse = db.getEntry(id: entryID)
id = entryToUse!.id
answer1 = entryToUse!.answer1.getNSMutableAttributedStringFromHTML() // Converts HTML from the DB to a NSMutableAttributedString
}
#Published var id: Int32
#Published var answer1: NSMutableAttributedString = NSMutableAttributedString() {
didSet {
print("NEW answer1: " + answer1.string)
}
}
}
I wonder if there is a way to bind the attributedString to the ViewModel?
Thanks a lot for the help.
Not sure if it's the correct way to do it, but I got the binding to work with the following code:
import Foundation
import SwiftUI
struct RichTextField: NSViewRepresentable {
typealias NSViewType = NSTextView
#Binding var attributedString: NSAttributedString
var isEditable: Bool
func makeCoordinator() -> Coordinator {
Coordinator(self)
}
func makeNSView(context: Context) -> NSTextView {
let textView = NSTextView(frame: .zero)
textView.textStorage?.setAttributedString(self.attributedString)
textView.isEditable = isEditable
textView.delegate = context.coordinator
textView.translatesAutoresizingMaskIntoConstraints = false
textView.autoresizingMask = [.width, .height]
return textView
}
func updateNSView(_ nsView: NSTextView, context: Context) {
nsView.textStorage!.setAttributedString(self.attributedString)
}
// Source: https://medium.com/fantageek/use-xib-de9d8a295757
class Coordinator: NSObject, NSTextViewDelegate {
let parent: RichTextField
init(_ RichTextField: RichTextField) {
self.parent = RichTextField
}
func textDidChange(_ notification: Notification) {
guard let textView = notification.object as? NSTextView else { return }
self.parent.attributedString = textView.attributedString()
}
}
}
(I still get the "cycle detected" error when using the RichTextField in a List (non-editable) though..)
I hope I'm wrong, but I have not been able to find a SwiftUI equivalent to an editable
UITextView. So, I built one using UIViewRepresentable. Populating both a SwiftUI Text
and my own view with the ObservableObject works - but updates made in my view are
not propagated to the ObservableObject. I must be missing something important with
the Binding concept. Any guidance would be appreciated.
import SwiftUI
import Combine
struct ContentView: View {
#ObservedObject var myOText: MyOText
var body: some View {
ScrollView {
VStack {
Text("This is a bound Text View")
.padding(.top, 10)
.font(.headline)
Text(myOText.inTheCourse)
.lineLimit(3)
.padding()
Text("This is a multi-line UITextView wrapper:")
.font(.headline)
MultilineTextView(myOText: myOText)
.frame(height: 100)
.padding()
Spacer()
}
}
}
}
struct MultilineTextView: UIViewRepresentable {
#ObservedObject var myOText: MyOText
func makeUIView(context: Context) -> UITextView {
let view = UITextView()
view.isScrollEnabled = true
view.isEditable = true
view.isUserInteractionEnabled = true
view.textAlignment = .center
view.font = UIFont(name: "Times New Roman", size: 20)
return view
}
func updateUIView(_ uiView: UITextView, context: Context) {
uiView.text = myOText.inTheCourse
}
}
class MyOText: ObservableObject {
#Published var inTheCourse: String = "When in the Course of human events, it becomes necessary for one people to dissolve the political bands which have connected them ..."
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView(myOText: MyOText())
}
}
Xcode Version 11.2 beta 2 (11B44), iOS 13.
Your multiline text view needs a coordinator to observe the text updates from UITextView
struct MultilineTextView: UIViewRepresentable {
#ObservedObject var myOText: MyOText
func makeUIView(context: Context) -> UITextView {
let view = UITextView()
view.isScrollEnabled = true
view.isEditable = true
view.isUserInteractionEnabled = true
view.textAlignment = .center
view.font = UIFont(name: "Times New Roman", size: 20)
view.delegate = context.coordinator
return view
}
func updateUIView(_ uiView: UITextView, context: Context) {
uiView.text = myOText.inTheCourse
}
func makeCoordinator() -> MultilineTextView.Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, UITextViewDelegate {
var control: MultilineTextView
init(_ control: MultilineTextView) {
self.control = control
}
func textViewDidChange(_ textView: UITextView) {
control.myOText.inTheCourse = textView.text
}
}
}
Right now I am able to have the user successfully take a picture using the camera and have their picture be uploaded into a imageview. However, if the user leaves that view controller when they eventually return the photo is no longer in that imageview. Is there anyway I am able to save the photo so that once a picture is taken and put into that imageview it is there until the user takes a different picture to replace it?
import UIKit
class ArsenalViewController: UIViewController,
UIImagePickerControllerDelegate, UINavigationControllerDelegate {
#IBOutlet var ball1: UIImageView!
override func viewDidLoad() {
super.viewDidLoad()
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
#IBAction func addPhoto1(_ sender: Any) {
if UIImagePickerController.isSourceTypeAvailable(UIImagePickerControllerSourceType.camera){
let imagePicker = UIImagePickerController()
imagePicker.delegate = self
imagePicker.sourceType = UIImagePickerControllerSourceType.camera
imagePicker.allowsEditing = false
self.present(imagePicker, animated: true, completion: nil)
}
}
func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [String : Any]) {
if let pickedImage = info[UIImagePickerControllerOriginalImage] as? UIImage {
ball1.contentMode = .scaleToFill
ball1.image = pickedImage
}
picker.dismiss(animated: true, completion: nil)
}
First you need to save the image data somewhere (i.e. UserDefaults, Core Data, a database) and then retrieve it. UserDefaults is easy, try this:
//Encoding
let imageData:NSData = UIImageJPEGRepresentation(pickedImage!)! as NSData
//Saved image
UserDefaults.standard.set(imageData, forKey: "savedImage")
//Decode
if let data = UserDefaults.standard.object(forKey: "savedImage") as! NSData{
myImageView.image = UIImage(data: data as Data)
}
Encoding and saving image should happen right after updating your image and decoding should be done once the view has loaded all of its elements (viewDidAppear method).
After encoding and saving the image once, you can decode it anytime, even if the user left the VC and came back.