I'm using a "RectGetter" to get the CGRect of a View.
Like this:
Text("Hello")
.background(RectGetter(rect: self.$rect))
struct RectGetter: View {
#Binding var rect: CGRect
var body: some View {
GeometryReader { proxy in
self.createView(proxy: proxy)
}
}
func createView(proxy: GeometryProxy) -> some View {
DispatchQueue.main.async {
self.rect = proxy.frame(in: .global) // crash here
}
return Rectangle().fill(Color.clear)
}
}
But sometimes I get a crash when setting the rect.
Thread 0 name:
Thread 0 Crashed:
0 libsystem_kernel.dylib 0x00000001a1ec5ec4 __pthread_kill + 8
1 libsystem_pthread.dylib 0x00000001a1de5724 pthread_kill$VARIANT$armv81 + 216 (pthread.c:1458)
2 libsystem_c.dylib 0x00000001a1d358c0 __abort + 112 (abort.c:147)
3 libsystem_c.dylib 0x00000001a1d35850 abort + 112 (abort.c:118)
4 AttributeGraph 0x00000001cce56548 0x1cce25000 + 202056
5 AttributeGraph 0x00000001cce2b980 0x1cce25000 + 27008
6 SwiftUI 0x00000001d89ce9b4 $s7SwiftUI27_PositionAwareLayoutContextV10dimensionsSo6CGSizeVvg + 56 (<compiler-generated>:0)
7 SwiftUI 0x00000001d8a43584 $sSo6CGSizeVIgd_ABIegr_TRTA + 24 (<compiler-generated>:0)
8 SwiftUI 0x00000001d8a43a54 $s7SwiftUI13GeometryProxyV4sync33_4C4BAC551E328ACCA9CD3748EDC0CC3ALLyxSgxyXElFxAA9ViewGraphCXEfU_... + 92 (GeometryReader.swift:126)
9 SwiftUI 0x00000001d8a43f20 $s7SwiftUI13GeometryProxyV4sync33_4C4BAC551E328ACCA9CD3748EDC0CC3ALLyxSgxyXElFxAA9ViewGraphCXEfU_... + 20 (<compiler-generated>:0)
10 SwiftUI 0x00000001d8c4842c $s7SwiftUI16ViewRendererHostPAAE06updateC5Graph4bodyqd__qd__AA0cG0CXE_tlF + 80 (ViewRendererHost.swift:64)
11 SwiftUI 0x00000001d8a438d4 $s7SwiftUI13GeometryProxyV5frame2inSo6CGRectVAA15CoordinateSpaceO_tF + 196 (GeometryReader.swift:125)
12 MyApp 0x0000000102cf5c54 closure #1 in RectGetter.createView(proxy:) + 128 (RectGetter.swift:22)
Is there some more reliable way (not crashing) to get the CGRect of a View?
Update: improved and simplified a possible solution to read rect of any view in any coordinate space via helper extension. Works since Xcode 11.1, retested with Xcode 13.3.
Main part:
func rectReader(_ binding: Binding<CGRect>, _ space: CoordinateSpace = .global) -> some View {
GeometryReader { (geometry) -> Color in
let rect = geometry.frame(in: space)
DispatchQueue.main.async {
binding.wrappedValue = rect
}
return .clear
}
}
Usage the same
Text("Test").background(rectReader($rect))
or with new extension
Text("Test").reading(rect: $rect)
Complete findings and code is here
Another way is with Preference Keys, as outlined here.
In summary, you can set up a modifier:
struct SizePreferenceKey: PreferenceKey {
static var defaultValue: CGRect = .zero
static func reduce(value: inout CGRect, nextValue: () -> CGRect) {
value = nextValue()
}
}
struct SizeModifier: ViewModifier {
private var sizeView: some View {
GeometryReader { geometry in
Color.clear.preference(key: SizePreferenceKey.self, value: geometry.frame(in: .global))
}
}
func body(content: Content) -> some View {
content.background(sizeView)
}
}
Then use that one:
SomeView()
.modifier(SizeModifier())
.onPreferenceChange(SizePreferenceKey.self) { print("My rect is: \($0)") }
Related
Im fairly new to Swift and I'm trying to produce a HStack (that will be used in a progress bar) of element and to be able to add elements with a button.
Im not sure if I should use a variable in the ForEach(1..<Variable) section or use another method. Here is the code I have so far but it did not work.
struct ContentView: View {
#State var fill : CGFloat = 0
#State var NumberOfCircles : Int = 0
var body: some View {
HStack(spacing:100) {
ForEach(0..<NumberOfCircles){ _ in
MyShape()
}
Button(action: {NumberOfCircles = 5}, label: {
Text("Button")
})
}
ForEach in SwiftUI needs a constant range to loop over. However,
as the error suggests, if you conform to Identifiable or use ForEach(_:id:content:) and provide an explicit id it is happy. So try this:
struct ContentView: View {
#State var fill: CGFloat = 0
#State var NumberOfCircles: Int = 0
var body: some View {
HStack(spacing: 20) {
ForEach(0..<NumberOfCircles, id: \.self){ _ in // <-- here
MyShape()
}
Button(action: {NumberOfCircles = 5}){
Text("Button")
}
}
}
}
I'm not sure what's your problem, but I tested this code and it works:
struct ContentView: View {
#State var numberOfCircles = 1
var body: some View {
VStack {
HStack {
ForEach(0..<numberOfCircles, id:\.self) { _ in
Circle()
.frame(width: 30, height: 30)
}
}
Button { numberOfCircles = 5 } label: {
Text("Add Circles")
}
}
}
}
Btw, naming convention for variables in Swift is camelCase. That means that declaring a variable you should name it numberOfCircles , not NumberOfCircles . The first uppercase letter is reserved for naming Classes, Structs and Protocols.
I trying to make a SwiftUI app where after entering one letter in a TextField the cursor automatically moves to the next TextField. The UI is pretty much like this.
In Swift/IB, it looks like this was done with delegates and adding a target like in this post:
How to move to the next UITextField automatically in Swift
But can't find any documentation for using delegates/targets in SwiftUI.
I tried following this post:
SwiftUI TextField max length
But this has not worked for me. Setting the .prefix(1) does not seem to make a difference. The TextField still accepts any amount of characters and when moved to the next TextField does not reduce the characters entered to only the first character.
In SwiftUI's current state, is it possible to automatically move to the next TextField after 1 character is entered?
Thanks for any help!
It can be done in iOS 15 with FocusState
import SwiftUI
///Sample usage
#available(iOS 15.0, *)
struct PinParentView: View {
#State var pin: Int = 12356
var body: some View {
VStack{
Text(pin.description)
PinView(pin: $pin)
}
}
}
#available(iOS 15.0, *)
struct PinView: View {
#Binding var pin: Int
#State var pinDict: [UniqueCharacter] = []
#FocusState private var focusedField: UniqueCharacter?
var body: some View{
HStack{
ForEach($pinDict, id: \.id, content: { $char in
TextField("pin digit", text:
Binding(get: {
char.char.description
}, set: { newValue in
let newest: Character = newValue.last ?? "0"
//This check is only needed if you only want numbers
if Int(newest.description) != nil{
char.char = newest
}
//Set the new focus
DispatchQueue.main.async {
setFocus()
}
})
).textFieldStyle(.roundedBorder)
.focused($focusedField, equals: char)
})
}.onAppear(perform: {
//Set the initial value of the text fields
//By using unique characters you can keep the order
pinDict = pin.description.uniqueCharacters()
})
}
func setFocus(){
//Default to the first box when focus is not set or the user reaches the last box
if focusedField == nil || focusedField == pinDict.last{
focusedField = pinDict.first
}else{
//find the index of the current character
let idx = pinDict.firstIndex(of: focusedField!)
//Another safety check for the index
if idx == nil || pinDict.last == pinDict[idx!]{
focusedField = pinDict.first
}else{
focusedField = pinDict[idx! + 1]
}
}
//Update the Binding that came from the parent
setPinBinding()
}
///Updates the binding from the parent
func setPinBinding(){
var newPinInt = 0
for n in pinDict{
if n == pinDict.first{
newPinInt = Int(n.char.description) ?? 0
}else{
newPinInt = Int(String(newPinInt) + n.char.description) ?? 0
}
}
pin = newPinInt
}
}
//Convert String to Unique characers
extension String{
func uniqueCharacters() -> [UniqueCharacter]{
let array: [Character] = Array(self)
return array.uniqueCharacters()
}
func numberOnly() -> String {
self.trimmingCharacters(in: CharacterSet(charactersIn: "-0123456789.").inverted)
}
}
extension Array where Element == Character {
func uniqueCharacters() -> [UniqueCharacter]{
var array: [UniqueCharacter] = []
for char in self{
array.append(UniqueCharacter(char: char))
}
return array
}
}
//String/Characters can be repeating so yu have to make them a unique value
struct UniqueCharacter: Identifiable, Equatable, Hashable{
var char: Character
var id: UUID = UUID()
}
#available(iOS 15.0, *)
struct PinView_Previews: PreviewProvider {
static var previews: some View {
PinParentView()
}
}
I'm creating a native UI component on iOS and I want its size to expand according to the content size.
It seems like I must set a fixed width and height in order for the view to be rendered. Any idea how to solve it?
// JS
import React from 'react';
import { View, requireNativeComponent } from 'react-native';
class StyledText extends React.Component {
render() {
return (
<View style={this.props.style}>
// without the height and width the compnent won't show up
<StyledLabelReactBridge styledText={'some text'} style={{height: 100, width: 100, backgroundColor: 'red'}}/>
</View>
);
}
}
StyledText.propTypes = {
styledText: React.PropTypes.string,
style: View.propTypes.style
};
const StyledLabelReactBridge = requireNativeComponent('StyledLabelReactBridge', StyledText);
module.exports = StyledText;
// objective-C
#implementation StyledLabelReactBridgeManager
RCT_EXPORT_MODULE()
- (UIView *)view
{
return [[NewStyledLabel alloc] init];
}
RCT_CUSTOM_VIEW_PROPERTY(styledText, NSString, NewStyledLabel)
{
if (![json isKindOfClass:[NSString class]])
return;
[view setStyledText:[NewStyledText textFromXHTML:json]];
}
#end
You need to override reactSetFrame in xcode to receive content size change.
#import "React/UIView+React.h"
#implementation YourView {
- (void)reactSetFrame:(CGRect)frame {
[super reactSetFrame: frame];
/* everytime content size changes, you will get its frame here. */
}
}
first you should create a subclass of RCTShadowView like
#import <React/RCTShadowView.h>
#interface RNGuessLikeContainerShadowView : RCTShadowView
#end
#implementation RNGuessLikeContainerShadowView
- (void)setLocalData:(NSObject *)localData {
if ([localData isKindOfClass:[NSNumber class]]) {
[self setIntrinsicContentSize:CGSizeMake(UIScreen.mainScreen.bounds.size.width, ((NSNumber *)localData).floatValue)];
}
}
#end
then create subclass of RCTViewManager and return shadowview and view of you custom class instance
#import <React/RCTUIManager.h>
#import <React/RCTUIManagerUtils.h>
#interface RNGuessLikeModule: RCTViewManager <RNGuessLikeContainerViewHeightUpdater>
#end
#implementation RNGuessLikeModule
RCT_EXPORT_MODULE(RNGuessLikeModule)
RCT_EXPORT_VIEW_PROPERTY(objects, NSString);
- (UIView *)view {
RNGuessLikeContainerView *_view = [RNGuessLikeContainerView new];
_view.delegate = self;
return _view;
}
- (RCTShadowView *)shadowView {
return [RNGuessLikeContainerShadowView new];
}
- (void)didUpdateWithHeight:(CGFloat)height view:(RNGuessLikeContainerView *)view {
RCTExecuteOnUIManagerQueue(^{
RCTShadowView *shadowView = [self.bridge.uiManager shadowViewForReactTag:view.reactTag];
[shadowView setLocalData:#(height)];
[self.bridge.uiManager setNeedsLayout];
});
}
#end
and in mine code i set custom native ui view delegate to RNGuessLikeModule which is subclass of RCTViewManager,
and you can caculate size in you custom view when data from rn module passed
#objc
public protocol RNGuessLikeContainerViewHeightUpdater {
func didUpdate(height: CGFloat, view: RNGuessLikeContainerView)
}
public final class RNGuessLikeContainerView: UIView, GuessLikeItemsComponentContainer {
#objc
public var objects: String? {
didSet {
if let _objects = objects,
let _data = _objects.data(using: .utf8, allowLossyConversion: true) {
reload(objects: _data)
}
}
}
#objc
public weak var delegate: RNGuessLikeContainerViewHeightUpdater?
public var controller: UIViewController {
return reactViewController()
}
public var guessLikeSceneType: GuessLikeSceneType = .邀好友赚现金红包
public var guessLikeTitle: String?
public var guessLikeItems: [GuessLikeItemsSectionSubItem] = []
public var routerInfo: String?
#objc
public private(set) var guessLikeHeight: CGFloat = 0
lazy var backend = GuessLikeItemsComponentContainerBackend(parent: self)
public lazy var guessLikeContainer: UICollectionView = {
let _container = createGuessLikeContainer()
_container.dataSource = backend
_container.delegate = backend
addSubview(_container)
return _container
}()
override public func layoutSubviews() {
super.layoutSubviews()
guessLikeContainer.frame = bounds
}
public func reload(objects: Data) {
precondition(pthread_main_np() != 0, "RNGuessLikeContainerView reload method should be called on main thread")
do {
let _items = try JSONDecoder().decode([ItemListModel].self, from: objects)
guessLikeItems = GuessLikeItemsSectionItem(list: _items).items
guessLikeContainer.reloadData()
updateHeight()
} catch {
debugPrint(error)
}
}
public func append(objects: Data) {
precondition(pthread_main_np() != 0, "RNGuessLikeContainerView append method should be called on main thread")
if let _list = try? JSONDecoder().decode([ItemListModel].self, from: objects) {
let _items = GuessLikeItemsSectionItem(list: _list).items
guessLikeItems.append(contentsOf: _items)
guessLikeContainer.reloadData()
updateHeight()
}
}
func updateHeight() {
if guessLikeItems.isEmpty {
guessLikeHeight = 0
} else {
var leftHeight: CGFloat = 0
var rightHeight: CGFloat = 0
for (index, item) in guessLikeItems.enumerated() {
if index % 2 == 0 {
leftHeight += item.height + 10.0
} else {
rightHeight += item.height + 10.0
}
}
let sectionHeaderHeight: CGFloat = 50.0
guessLikeHeight = max(leftHeight, rightHeight) + sectionHeaderHeight
}
if let _delegate = delegate {
_delegate.didUpdate(height: guessLikeHeight, view: self)
}
}
public override var intrinsicContentSize: CGSize {
return CGSize(width: UIScreen.main.bounds.width, height: guessLikeHeight)
}
}
then find shadowview binded to your custom ui view and update intrinsicContentSize
finaly call [self.bridge.uiManager setNeedsLayout]
may help you
#implementation SNBFundCardViewManager
RCT_EXPORT_MODULE(FundCard)
- (UIView *)view
{
return [[SNBFundHomeFundCardCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:#""];
}
- (dispatch_queue_t)methodQueue
{
return dispatch_get_main_queue();
}
RCT_CUSTOM_VIEW_PROPERTY(data, NSDictionary, SNBFundHomeFundCardCell)
{
view.rnData = json;
// 自己撑起高度
CGFloat height = [view.vm getCellHeight];
[self.bridge.uiManager setIntrinsicContentSize:CGSizeMake(UIViewNoIntrinsicMetric, height) forView:view];
}
Well, couldn't find 'auto' like behavior, how ever, setting the component to:
{{ width: '100%', height: '100%}}
Makes it expand (and shrink) according to the parent, which is good enough for my use case. It's a shame that setting 'flex: 1' doesn't have the same effect.
I have update my Project to Swift3 in Xcode 8 and it comes this error but I have no idea what I can make there. I have already search in google but nothing founded.
Have anyone an Idea what I can make ?
Here the Error:
Method 'collectionViewContentSize()' with Objective-C selector 'collectionViewContentSize' conflicts with getter for 'collectionViewContentSize' from superclass 'UICollectionViewLayout' with the same Objective-C selector
public func collectionViewContentSize() -> CGSize {
let numberOfSections = collectionView?.numberOfSections
if numberOfSections == 0 {
return CGSize.zero
}
var contentSize = collectionView?.bounds.size
contentSize?.height = CGFloat(columnHeights[0])
return contentSize!
}
I had something similar but I was overriding collectionViewContentSize()
override func collectionViewContentSize() -> CGSize {
let collection = collectionView!
let width = collection.bounds.size.width
let height = max(posYColumn1, posYColumn2)
return CGSize(width: width, height: height)
}
I Downloaded XCode 8 beta 4 today and have had to change it to:
override var collectionViewContentSize: CGSize {
let collection = collectionView!
let width = collection.bounds.size.width
let height = max(posYColumn1, posYColumn2)
return CGSize(width: width, height: height)
}
I'm trying to implement Pinch & Rotate gestures on a UIView who contains several subviews(UIButton,UITextView).
Code organized below to your convenience
I'm guessing i'm missing something. No idea what tho. Thank you!
Result :
Code
Scale :
var scaleAnchorPoint = CGPoint()
func handleSizeIncreasing(sender:UIPinchGestureRecognizer) {
if sender.state == UIGestureRecognizerState.Began
{
print("Began")
scaleAnchorPoint = self.center
}
else if sender.state == UIGestureRecognizerState.Changed
{
txtView.transform = CGAffineTransformScale(txtView.transform, sender.scale, sender.scale)
sender.scale = 1.0
self.frame.size = CGSizeMake(txtView.frame.width + buttonSize, txtView.frame.height + buttonSize)
self.center = scaleAnchorPoint
updateViews(scaleAnchorPoint)
print("Changed")
}
}
Rotate :
var rotateAnchorPoint = CGPoint()
func handleRotate(sender : UIRotationGestureRecognizer) {
if sender.state == UIGestureRecognizerState.Began
{
print("Began")
rotateAnchorPoint = self.center
}
else if sender.state == UIGestureRecognizerState.Changed
{
sender.view!.transform = CGAffineTransformRotate(sender.view!.transform, sender.rotation)
sender.rotation = 0
updateViews(rotateAnchorPoint)
print("Changed")
}
}
Side:
func updateViews(aroundPoint : CGPoint )
{
self.center = aroundPoint
txtView.frame.origin = CGPointMake(buttonSize / 2, buttonSize / 2)
txtView.contentSize = txtView.frame.size
}
func gestureRecognizer(gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWithGestureRecognizer otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true
}
Figured it out. Simply, when the scale/frame changes, instead of accessing the Frame like i did here : self.frame.size = CGSizeMake(txtView.frame.width + buttonSize, txtView.frame.height + buttonSize)
Access self.bounds , like this -
self.bounds.size = CGSizeMake(txtView.frame.width + buttonSize, txtView.frame.height + buttonSize)
Enjoy!