I've been trying to follow a tutorial on embedding an AVCaptureVideoPreviewLayer (or in simpler terms a very basic viewfinder using camera) and have had trouble converting the iOS code to fit my MacOS application.
The issue appeared when I attempted to convert this block of code:
// setting view for preview...
struct CameraPreview: UIViewRepresentable {
#ObservedObject var camera : CameraModel
func makeUIView(context: Context) -> UIView {
let view = UIView(frame: UIScreen.main.bounds)
camera.preview = AVCaptureVideoPreviewLayer(session: camera.session)
camera.preview.frame = view.frame
// Your Own Properties...
camera.preview.videoGravity = .resizeAspectFill
view.layer.addSublayer(camera.preview)
// starting session
camera.session.startRunning()
return view
}
func updateUIView(_ uiView: UIView, context: Context) {
}
}
My understanding is that UIViews are only for iOS development and that MacOS uses NSViews instead. Knowing this I did my best to come up with the following modified code
// setting view for preview...
struct CameraPreview: NSViewRepresentable { // ERROR #1
#ObservedObject var camera : CameraModel
func makeNSView(context: Context) -> NSView {
let view = NSView(frame: CGRect(origin: .zero,
size: CGSize(width: 200, height: 200)))
camera.preview = AVCaptureVideoPreviewLayer(session: camera.session)
camera.preview.frame = view.frame
// Your Own Properties...
camera.preview.videoGravity = .resizeAspectFill
view.addSubview(camera.preview) //ERROR #2
// starting session
camera.session.startRunning()
return view
}
func updateUIView(_ uiView: NSView, context: NSView) {
}
}
The issues are:
Type 'CameraPreview' does not conform to protocol 'NSViewRepresentable' (ERROR #1)
Cannot convert value of type 'AVCaptureVideoPreviewLayer' to expected argument type 'NSView' (ERROR #2)
I'm an intermediate iOS developer, but have been trying to build my skills in MacOS development.
You forgot to update this part updateUIView
func updateNSView(_ uiView: NSView, context: Context) { // << here !!
}
about second... you try to add layer to view, but layout should be added to layer, like
let view = NSView(frame: CGRect(origin: .zero,
size: CGSize(width: 200, height: 200)))
view.wantsLayer = true // needed explicitly for NSView
...
view.layer.addSublayer(camera.preview)
Related
I tried to connect a native AdMob to my swiftui project. I followed the google documentation to the letter but my compiler displays this error
Could not load NIB in bundle: 'NSBundle </private/var/.../' with name
'NativeAdView'
I've searched everywhere and I can't find any explanation. If someone could help me I'm interested !
struct ContentView: View {
#StateObject private var viewModel = ViewModel()
var body: some View {
NativeAdView(nativeAdViewModel: viewModel)
}
}
struct NativeAdView: UIViewRepresentable {
typealias UIViewType = GADNativeAdView
#ObservedObject var nativeAdViewModel: AdViewModel
func makeUIView(context: Context) -> GADNativeAdView {
// Link the outlets to the views in the GADNativeAdView.
return
Bundle.main.loadNibNamed(
"NativeAdView",
owner: nil,
options: nil)?.first as! GADNativeAdView
// return GADNativeAdView(frame: CGRect(x: 0, y: 0, width: 200, height: 100))
}
func updateUIView(_ nativeAdView: GADNativeAdView, context: Context) {
guard let nativeAd = nativeAdViewModel.nativeAd else { return }
// Work with your native ad.
nativeAdView.mediaView?.mediaContent = nativeAd.mediaContent
}
}
I tried to change the GADNativeAdView and it'll work. I mean the problem come from the return bundle.main.loadNibNamed(...) but I can't find the right way to do it.
I am trying to take a SwiftUI view and render it to an image for my application. Currently I am using the following code https://www.hackingwithswift.com/example-code/media/how-to-render-a-uiview-to-a-uiimage and a UIHostingController, however I get weird results with the proper view looking correct, but the generated image not looking correct.
Here is a picture of what I see,
iOS 16 Update
Note: iOS 16 has ImageRenderer now see https://www.hackingwithswift.com/quick-start/swiftui/how-to-convert-a-swiftui-view-to-an-image . I do not know if it has the same issues but the below code should work with versions of iOS before 16.
Original Post
The first failure (where the view does not render at all) is because the drawHierarchy fails, seemingly because it isn't run after onAppear (I think).
The second one is because the UIHostingController introduces safe area inset padding (see Cannot place SwiftUI view outside the SafeArea when embedded in UIHostingController )
Here is a sample image with a working example (see code below):
Here is an example code with the last one working:
import SwiftUI
struct ContentView: View {
// normal view version using Text
#State
var textView: AnyView
// uiImage created before onAppear
#State
var imageBeforeOnAppear: UIImage?
// uiImage displayed using image without safe area insets
#State
var imageWithoutSafeArea: UIImage?
// uiImage displayed using image with safe area insets
#State
var imageWithSafeArea: UIImage?
#State
var show = false
var body: some View{
VStack{
if (!show){
Button(action: {
self.render()
self.show.toggle()
}, label: {Text("Show")})
}
if (show){
Text("Normal View")
self.textView.border(Color.red, width: 5)
Text("Image created in init (drawHierarchy fails)")
Image(uiImage: self.imageBeforeOnAppear!).border(Color.red, width: 5)
Text("Image created without SafeArea being disabled")
Image(uiImage: self.imageWithoutSafeArea!).border(Color.red, width: 5)
Text("Image created with SafeArea being disabled")
Image(uiImage: self.imageWithSafeArea!).border(Color.red, width: 5)
}
}
}
init(){
print("Creating content view...")
// generate a nice looking date for our text
let localDateFormatter = DateFormatter();
localDateFormatter.dateStyle = DateFormatter.Style.short
localDateFormatter.timeStyle = DateFormatter.Style.medium
localDateFormatter.string(from: Date())
let textString = "Generated at \(localDateFormatter.string(from: Date()))"
print(textString)
self.textView = AnyView(Text(textString)
.foregroundColor(Color.white)
.padding()
.background(Color.purple))
let iboa = ContentView.toImage(view: textView, disableSafeArea: true)
print("Image size \(iboa.size)")
// see https://stackoverflow.com/questions/56691630/swiftui-state-var-initialization-issue
// force this to be initialized in init
// this will NOT work:
// self.imageBeforeOnAppear = iboa
// it will still be null
self._imageBeforeOnAppear = State(initialValue: iboa)
}
// Need to call this after the ContentView is showing!
// That's why we have a button to delay this (and not
// run it on init for example) otherwise drawHierarchy
// will fail if you set afterScreenUpdates to true!
// (and it won't render anything if you set afterScreenUpdates
// to false)
func render(){
// generate image version with safe area disabled
self.imageWithSafeArea = ContentView.toImage(view: textView)
print("Image size \(self.imageWithSafeArea!.size)")
// generate image version without safe area disabled
self.imageWithoutSafeArea = ContentView.toImage(view: textView, disableSafeArea: false)
print("Image size \(self.imageWithoutSafeArea!.size)")
}
static func toImage(view: AnyView, disableSafeArea: Bool = true) -> UIImage{
print("Thread info \(Thread.current)")
print("Thread main? \(Thread.current.isMainThread)")
let controller = UIHostingController(rootView:view)
if (disableSafeArea){
// otherwise there is a buffer at the top of the frame
controller.disableSafeArea()
}
controller.view.setNeedsLayout()
controller.view.layoutIfNeeded()
let targetSize = controller.view.intrinsicContentSize
print("Image Target Size \(targetSize)")
let rect = CGRect(x: 0,
y: 0,
width: targetSize.width,
height:targetSize.height)
controller.view.bounds = rect
controller.view.frame = rect
// so we at least see something if the SwiftUI view
// fails to render
controller.view.backgroundColor = UIColor.green
let renderer = UIGraphicsImageRenderer(size: targetSize)
let image = renderer.image(actions: { rendererContext in
controller.view.layer.render(in: rendererContext.cgContext)
let result = controller.view.drawHierarchy(in: controller.view.bounds,
afterScreenUpdates: true)
print("drawHierarchy successful? \(result)")
})
return image
}
}
// see https://stackoverflow.com/questions/70156299/cannot-place-swiftui-view-outside-the-safearea-when-embedded-in-uihostingcontrol
extension UIHostingController {
convenience public init(rootView: Content, ignoreSafeArea: Bool) {
self.init(rootView: rootView)
if ignoreSafeArea {
disableSafeArea()
}
}
func disableSafeArea() {
guard let viewClass = object_getClass(view) else { return }
let viewSubclassName = String(cString: class_getName(viewClass)).appending("_IgnoreSafeArea")
if let viewSubclass = NSClassFromString(viewSubclassName) {
object_setClass(view, viewSubclass)
}
else {
guard let viewClassNameUtf8 = (viewSubclassName as NSString).utf8String else { return }
guard let viewSubclass = objc_allocateClassPair(viewClass, viewClassNameUtf8, 0) else { return }
if let method = class_getInstanceMethod(UIView.self, #selector(getter: UIView.safeAreaInsets)) {
let safeAreaInsets: #convention(block) (AnyObject) -> UIEdgeInsets = { _ in
return .zero
}
class_addMethod(viewSubclass, #selector(getter: UIView.safeAreaInsets), imp_implementationWithBlock(safeAreaInsets), method_getTypeEncoding(method))
}
objc_registerClassPair(viewSubclass)
object_setClass(view, viewSubclass)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
ContentView()
}
}
I am writing a music player app using apple Media Player and have a button to change the volume and hide the system default volume overlay.
However, all methods I find are based on UIKit.
Like
let volumeView = MPVolumeView(frame: .zero)
volumeView.clipsToBounds = true
volumeView.alpha = 0.00001
volumeView.showsVolumeSlider = false
view.addSubview(volumeView)
And I tried
import Foundation
import SwiftUI
import MediaPlayer
struct VolumeView:UIViewRepresentable{
let volumeView:MPVolumeView
func makeUIView(context: Context) -> MPVolumeView {
volumeView.clipsToBounds = true
volumeView.alpha = 0.00001
volumeView.showsVolumeSlider = false
return volumeView
}
func updateUIView(_ uiView: MPVolumeView, context: Context) {
}
typealias UIViewType = MPVolumeView
}
It receives the MPVolumeView I created in my view model and I place it in a swiftui view. The indicator disappears but it can't change the volume.
Then I tried to make a new instance of MPVolumeView in UIViewRepresentable and it also didn't work.
I am a green hand in swiftui, can anybody help me?
I had the same question and your example helped me to figure it out, you were nearly there apparently.
So here is a complete custom Volume Slider in SwiftUI
struct VolumeSliderView: View {
var primaryColor: Color
#StateObject var volumeViewModel = VolumeViewModel()
var body: some View {
ZStack {
VolumeView()
.frame(width: 0, height: 0)
Slider(value: $volumeViewModel.volume, in: 0...1) {
Text("")
} minimumValueLabel: {
Image(systemName: "speaker.fill")
.font(.callout)
.foregroundColor(primaryColor.opacity(0.6))
} maximumValueLabel: {
Image(systemName: "speaker.wave.3.fill")
.font(.callout)
.foregroundColor(primaryColor.opacity(0.6))
} onEditingChanged: { isEditing in
volumeViewModel.setVolume()
}
.tint(primaryColor.opacity(0.6))
}
}
}
import Foundation
import MediaPlayer
struct VolumeView: UIViewRepresentable{
func makeUIView(context: Context) -> MPVolumeView {
let volumeView = MPVolumeView(frame: CGRect.zero)
volumeView.alpha = 0.001
return volumeView
}
func updateUIView(_ uiView: MPVolumeView, context: Context) {
}
}
It seems that an instance of MPVolumeView needs to be present on the screen at any time, even if it's not visible. Seems a bit hacky but works. If I only hide the view in the setVolume() function and even call volumeView.showsVolumeSlider = false there, it won't make a difference.
class VolumeViewModel: ObservableObject {
#Published var volume: Float = 0.5
init() {
setInitialVolume()
}
private func setInitialVolume() {
volume = AVAudioSession().outputVolume
}
func setVolume() {
let volumeView = MPVolumeView()
let slider = volumeView.subviews.first(where: { $0 is UISlider }) as? UISlider
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now() + 0.03) {
slider?.setValue(self.volume, animated: false)
}
}
}
Hope it helps somebody, as I just found non working examples here on SO.
Given the following example code:
struct ActivityIndicatorView : UIViewRepresentable {
var style: UIActivityIndicatorView.Style = .medium
func makeUIView(context: UIViewRepresentableContext<ActivityIndicatorView>) -> UIActivityIndicatorView {
return UIActivityIndicatorView(style: style)
}
func updateUIView(_ uiView: UIActivityIndicatorView, context: UIViewRepresentableContext<ActivityIndicatorView>) {
uiView.color = UIColor.white // how would I set this to the current .foregroundColor value?
}
}
How do I find out the current value of .foregroundColor(…) in order to render my UIView correctly?
I have read this question but that is from the perspective of inspecting the ModifiedContent from the outside, not the wrapped View.
There is no way to access the foreground colour but you can access the colour scheme and determine the colour of your activity indicator based on that:
struct ActivityIndicatorView : UIViewRepresentable {
#Environment(\.colorScheme) var colorScheme: ColorScheme
//...
func updateUIView(_ uiView: UIActivityIndicatorView, context: UIViewRepresentableContext<ActivityIndicatorView>) {
switch colorScheme {
case .dark:
uiView.color = .white
case .light:
uiView.color = .black
}
}
}
I just wanted to create a view and when it shown then the whole background will be dimmed like an alert view controller. If it is possible then please guide me and if possible then provide me code.
Thank you
The simplest way for doing that is to add a semi-transparent background (e.g. black with alpha less than 1.0) view, which contains the alert view. The background view should cover all other views in the view controller.
You can also use a modal view controller which has such a background view as its view, and presenting this controller with presentation style Over Full Screen.
// Here is the wrapper code i use in most of my project now a days
protocol TransparentBackgroundProtocol {
associatedtype ContainedView
var containedNib: ContainedView? { get set }
}
extension TransparentBackgroundProtocol where ContainedView: UIView {
func dismiss() {
containedNib?.superview?.removeFromSuperview()
containedNib?.removeFromSuperview()
}
mutating func add(withFrame frame: CGRect, toView view: UIView, backGroundViewAlpha: CGFloat) {
containedNib?.frame = frame
let backgroundView = configureABlackBackGroundView(alpha: backGroundViewAlpha)
view.addSubview(backgroundView)
guard let containedNib = containedNib else {
print("No ContainedNib")
return
}
backgroundView.addSubview(containedNib)
}
private func configureABlackBackGroundView(alpha: CGFloat) -> UIView {
let blackBackgroundView = UIView(frame: CGRect(x: 0, y: 0, width: UIScreen.main.bounds.size.width, height: UIScreen.main.bounds.size.height))
blackBackgroundView.backgroundColor = UIColor.black.withAlphaComponent(alpha)
return blackBackgroundView
}
}
// Sample View shown like alertView
class LogoutPopUpView: UIView, TransparentBackgroundProtocol {
// MARK: Variables
weak var containedNib: LogoutPopUpView?
typealias ContainedView = LogoutPopUpView
// MARK: Outlets
// MARK: Functions
class func initiate() -> LogoutPopUpView {
guard let nibView = Bundle.main.loadNibNamed("LogoutPopUpView", owner: self, options: nil)?[0] as? LogoutPopUpView else {
fatalError("Cann't able to load nib file.")
}
return nibView
}
}
// where u want to show pop Up
logOutPopup = LogoutPopUpView.instanciateFromNib()
let view = UIApplication.shared.keyWindow?.rootViewController?.view {
logOutPopup?.add(withFrame: CGRect(x: 30, y:(UIScreen.main.bounds.size.height-340)/2, width: UIScreen.main.bounds.size.width - 60, height: 300), toView: view, backGroundViewAlpha: 0.8)
}
// for dismiss
self.logOutPopup?.dismiss()