Add automatic sizeThatFits computation for views - #216

Merged
lucdion merged 23 commits into
layoutBox:masterfrom
antoinelamy:feature/AutoSizing
Jun 30, 2020
Merged

Add automatic sizeThatFits computation for views#216
lucdion merged 23 commits into
layoutBox:masterfrom
antoinelamy:feature/AutoSizing

Conversation

@antoinelamy

Copy link
Copy Markdown
Contributor

Disclaimer

This is a preliminary PR for discussion purpose. The implemented functionality has barely been tested and is not guaranteed to be bug free.

Motivation

Implementing sizeThatFits(_ size: CGSize) as part of the manual layout process has always been cumbersome. You always end up writing the same code twice, a first time for the layout and the second time for sizing. Using PinLayout to compute the resulting size like showcased in the AdjustToContainer exemple is not recommended either because the view coordinates are modified during sizing. The sizeThatFits method documentation state clearly:

This method does not resize the receiver.

Proposal

Build an autosizing mechanism on top of PinLayout without modifying the view's coordinates in the process.

  • This PR add the AutoSizeCalculable interface that defines autosizing related functions and properties.
  • An internal flag (Pin.autoSizingInProgress) is also needed for the layout system to know if it should compute an additional rect including the margins.
  • Implementing sizeThatFits(_ size: CGSize) using automatic sizing requires the following:
    • Layout code is preferably located in a separate function than layoutSubviews() to be sure things like setting the content size on a scroll view are not executed during the sizing process. In the provided exemple, that function would be layout().
    • The sizeThatFits implementation is as simple as calling return autoSizeThatFits(size) { layout() } and even takes into account the outer margins (ie: the bottom margin applied to the last view on y axis)

Limitations

  • Sadly I don't see any way of adding this capability directly on Layoutable because of the need to define stored properties.
  • All layout related code must be done using PinLayout only, otherwise the views that uses another layout system would be ignored in the resulting computed size.

Discussion

I would very much like feedback on this PR discussing the concept and exposing the potential flaws if any. I think that if we can get this thing to work properly in every situation it would be a great addition to PinLayout.

@antoinelamy
antoinelamy marked this pull request as draft May 25, 2020 01:02

@lucdionlucdion left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Salut Antoine 🙂

I like the general idea of your PR.

Global variable Pin.autoSizingInProgress

The only thing that bothers me is Pin.autoSizingInProgress. I know that all layouts related code should be executed from the main thread, so, a well-programmed app should not have 2 views that are layouted simultaneously. So having a global variable that contains that state shouldn't be an issue. But still, it's a global variable.

I'm not sure what could be a nicer solution thought, or if there is such a solution. But here is some thought on it (maybe this will trigger other ideas on your side 🤞).

Solution 1: Add a layout context
Suppose we add a method pin(_ context: LayoutContext?) and we add a context parameter to the layoutClosure, this context could then be passed to all pin calls. Ex:

override func sizeThatFits(_ size: CGSize) -> CGSize {
return autoSizeThatFits(size) { (ctx: LayoutContext) in layout(ctx) }
}
private func layout(_ ctx: LayoutContext?) {
subview.pin(ctx).top().left(10).width(200);
}

Cons:

  • A little verbose and not as nice.
  • It's easy to forget to call pin(_ context: LayoutContext?) instead of .pin.

Solution 2: Check view's parents
Check in the view hierarchy if there is a parent view with an autoSizingRect value, in that case, autoSizing is in progress. For this to work, we need to set to nil the property autoSizingRect when leaving autoSizeThatFits(...).

Cons:

  • Need to scans all view's parents to detect that there is no autoSizing in progress 😞

Solution 3: Global variable
Keep the global variable 😕

Experimental feature

In all cases I would keep that feature as experimental, i.e. we don't document immediately until you have played with the feature and you are happy with the result. During that time, you could note anything that would be useful to eventually document.

Non main thread calls warnings

I would also add a warning when .pin is called from a thread other than the main thread (https://github.com/layoutBox/PinLayout#pinlayouts-warnings).
Ex:
⚠️ PinLayout should be used only from the main thread. UIKit calls must be called from the app's main thread.

This warning could be disabled Pin.activeWarnings.mainThread.

...

I will continue to think about this feauture, so I may add other comments later

Comment threadSources/Impl/PinLayout+Layouting.swift Outdated
Comment threadSources/Extensions/UIView+PinLayout.swift Outdated
Comment threadSources/Extensions/UIView+PinLayout.swift Outdated
}
}

public func autoSizeThatFits(_ size: CGSize, layoutClosure: () -> Void) -> CGSize {

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really would like to extract that function out of the UIView conformance extension but the only thing that prevent me to do so is the call to let adjustedRect = Coordinates<View>.adjustRectToDisplayScale(rect). I think it would make sense to add a displayScale property on the Layoutable protocol and perform that transformation before calling setRect. We really want the resulting rect to be the same in both layout and auto sizing.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have perform the changes to do this but I believe it would make more sense in a separate PR as it also fix an issue in the current production code.

@antoinelamy

antoinelamy commented Jun 5, 2020

Copy link
Copy Markdown
ContributorAuthor

I agree that a global feature flag is probably not the most elegant solution but at the same time it's the less invasive way to implement it. I started trying to implement it creating a Pin object for a different object type acting as a view proxy but the code was overly complex and I ended up having the same need for a state variable.

Solution 1
It would require all the existing layout code to be refactored to opt-in to the feature plus it loose a bit of its simplicity that makes PinLayout so appealing IMO and like you said it would be easy to forget passing in the context object during pin creation.

Solution 2
I fear that the performance would take a huge hit on that one.

Solution 3
Well, I think it's the less bad solution and layout code should not be called from non main thread anyway, setting bounds or position on a background thread is forbidden. It might be a problem for another unknown Layoutable type that could potentially be layouted on multiple threads. In that case, my advise would be to not make that type conform to AutoSizeable.

The API is a bit similar to UIView.animate that wraps a beginAnimation() / endAnimation() behind the scene and a global flag is used to know wether or not the change should be performed animated or not. The fact that the API uses a closure where the layout code is expected to be called makes it less error prone I think.

@lucdion

Copy link
Copy Markdown
Member

I agree @antoinelamy, that your solution is probably the best, even if there is one flaw, its the one that has a minimal impact on code

@antoinelamy

Copy link
Copy Markdown
ContributorAuthor

Non main thread calls warnings

I would also add a warning when .pin is called from a thread other than the main thread (https://github.com/layoutBox/PinLayout#pinlayouts-warnings).
Ex:
⚠️ PinLayout should be used only from the main thread. UIKit calls must be called from the app's main thread.

This warning could be disabled Pin.activeWarnings.mainThread.

From what I see there is already such warning in PinLayout+Warning.displayLayoutWarnings() and it already covers the case of autosizing as we enter the apply() function the same way regular layout does:

if !Thread.isMainThread {
warn("Layout must be executed from the Main Thread!")
}

@antoinelamy
antoinelamy marked this pull request as ready for review June 16, 2020 11:49
@antoinelamyantoinelamy changed the title WIP: Add automatic sizeThatFits computation for viewsAdd automatic sizeThatFits computation for viewsJun 16, 2020
@antoinelamy

Copy link
Copy Markdown
ContributorAuthor

Should be good to go now @lucdion, anything else comes to mind?

Comment threadSources/Impl/PinLayout+Layouting.swift Outdated
@lucdion

Copy link
Copy Markdown
Member

Two more things:

  1. Could you duplicate the sample that you had previously modified? At least it shows an example.
  2. You won't like that, but even if its an experimental feature. We would need a minimalist documentation that describes the feature and how to use it. I would add that new section below https://github.com/layoutBox/PinLayout#justify--align
    Thanks

@antoinelamy

Copy link
Copy Markdown
ContributorAuthor

@lucdion I thought you preferred to silent release this feature, my mistake. I added a proper sample that fetches a random text and a random sized image. That content is then layout in a container that uses autosizing to determine its proper size. I also included setting a scroll view content size to illustrate the fact that we should not call non PinLayout code in the layout closure passed to autoSizeThatFits. I also added some documentation in the Readme for it, not sure if it's good enough but I'll leave it to you for review.

@lucdionlucdion left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ready to go 🎉
Thanks @antoinelamy for this nice addition

@lucdion
lucdion merged commit 21e53ef into layoutBox:masterJun 30, 2020
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@antoinelamy@lucdion
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Add automatic sizeThatFits computation for views - #216

Merged
lucdion merged 23 commits into
layoutBox:masterfrom
antoinelamy:feature/AutoSizing
Jun 30, 2020
Merged

Add automatic sizeThatFits computation for views#216
lucdion merged 23 commits into
layoutBox:masterfrom
antoinelamy:feature/AutoSizing

Conversation

@antoinelamy

Copy link
Copy Markdown
Contributor

Disclaimer

This is a preliminary PR for discussion purpose. The implemented functionality has barely been tested and is not guaranteed to be bug free.

Motivation

Implementing sizeThatFits(_ size: CGSize) as part of the manual layout process has always been cumbersome. You always end up writing the same code twice, a first time for the layout and the second time for sizing. Using PinLayout to compute the resulting size like showcased in the AdjustToContainer exemple is not recommended either because the view coordinates are modified during sizing. The sizeThatFits method documentation state clearly:

This method does not resize the receiver.

Proposal

Build an autosizing mechanism on top of PinLayout without modifying the view's coordinates in the process.

  • This PR add the AutoSizeCalculable interface that defines autosizing related functions and properties.
  • An internal flag (Pin.autoSizingInProgress) is also needed for the layout system to know if it should compute an additional rect including the margins.
  • Implementing sizeThatFits(_ size: CGSize) using automatic sizing requires the following:
    • Layout code is preferably located in a separate function than layoutSubviews() to be sure things like setting the content size on a scroll view are not executed during the sizing process. In the provided exemple, that function would be layout().
    • The sizeThatFits implementation is as simple as calling return autoSizeThatFits(size) { layout() } and even takes into account the outer margins (ie: the bottom margin applied to the last view on y axis)

Limitations

  • Sadly I don't see any way of adding this capability directly on Layoutable because of the need to define stored properties.
  • All layout related code must be done using PinLayout only, otherwise the views that uses another layout system would be ignored in the resulting computed size.

Discussion

I would very much like feedback on this PR discussing the concept and exposing the potential flaws if any. I think that if we can get this thing to work properly in every situation it would be a great addition to PinLayout.

@antoinelamy
antoinelamy marked this pull request as draft May 25, 2020 01:02

@lucdionlucdion left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Salut Antoine 🙂

I like the general idea of your PR.

Global variable Pin.autoSizingInProgress

The only thing that bothers me is Pin.autoSizingInProgress. I know that all layouts related code should be executed from the main thread, so, a well-programmed app should not have 2 views that are layouted simultaneously. So having a global variable that contains that state shouldn't be an issue. But still, it's a global variable.

I'm not sure what could be a nicer solution thought, or if there is such a solution. But here is some thought on it (maybe this will trigger other ideas on your side 🤞).

Solution 1: Add a layout context
Suppose we add a method pin(_ context: LayoutContext?) and we add a context parameter to the layoutClosure, this context could then be passed to all pin calls. Ex:

override func sizeThatFits(_ size: CGSize) -> CGSize {
return autoSizeThatFits(size) { (ctx: LayoutContext) in layout(ctx) }
}
private func layout(_ ctx: LayoutContext?) {
subview.pin(ctx).top().left(10).width(200);
}

Cons:

  • A little verbose and not as nice.
  • It's easy to forget to call pin(_ context: LayoutContext?) instead of .pin.

Solution 2: Check view's parents
Check in the view hierarchy if there is a parent view with an autoSizingRect value, in that case, autoSizing is in progress. For this to work, we need to set to nil the property autoSizingRect when leaving autoSizeThatFits(...).

Cons:

  • Need to scans all view's parents to detect that there is no autoSizing in progress 😞

Solution 3: Global variable
Keep the global variable 😕

Experimental feature

In all cases I would keep that feature as experimental, i.e. we don't document immediately until you have played with the feature and you are happy with the result. During that time, you could note anything that would be useful to eventually document.

Non main thread calls warnings

I would also add a warning when .pin is called from a thread other than the main thread (https://github.com/layoutBox/PinLayout#pinlayouts-warnings).
Ex:
⚠️ PinLayout should be used only from the main thread. UIKit calls must be called from the app's main thread.

This warning could be disabled Pin.activeWarnings.mainThread.

...

I will continue to think about this feauture, so I may add other comments later

Comment threadSources/Impl/PinLayout+Layouting.swift Outdated
Comment threadSources/Extensions/UIView+PinLayout.swift Outdated
Comment threadSources/Extensions/UIView+PinLayout.swift Outdated
}
}

public func autoSizeThatFits(_ size: CGSize, layoutClosure: () -> Void) -> CGSize {

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really would like to extract that function out of the UIView conformance extension but the only thing that prevent me to do so is the call to let adjustedRect = Coordinates<View>.adjustRectToDisplayScale(rect). I think it would make sense to add a displayScale property on the Layoutable protocol and perform that transformation before calling setRect. We really want the resulting rect to be the same in both layout and auto sizing.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have perform the changes to do this but I believe it would make more sense in a separate PR as it also fix an issue in the current production code.

@antoinelamy

antoinelamy commented Jun 5, 2020

Copy link
Copy Markdown
ContributorAuthor

I agree that a global feature flag is probably not the most elegant solution but at the same time it's the less invasive way to implement it. I started trying to implement it creating a Pin object for a different object type acting as a view proxy but the code was overly complex and I ended up having the same need for a state variable.

Solution 1
It would require all the existing layout code to be refactored to opt-in to the feature plus it loose a bit of its simplicity that makes PinLayout so appealing IMO and like you said it would be easy to forget passing in the context object during pin creation.

Solution 2
I fear that the performance would take a huge hit on that one.

Solution 3
Well, I think it's the less bad solution and layout code should not be called from non main thread anyway, setting bounds or position on a background thread is forbidden. It might be a problem for another unknown Layoutable type that could potentially be layouted on multiple threads. In that case, my advise would be to not make that type conform to AutoSizeable.

The API is a bit similar to UIView.animate that wraps a beginAnimation() / endAnimation() behind the scene and a global flag is used to know wether or not the change should be performed animated or not. The fact that the API uses a closure where the layout code is expected to be called makes it less error prone I think.

@lucdion

Copy link
Copy Markdown
Member

I agree @antoinelamy, that your solution is probably the best, even if there is one flaw, its the one that has a minimal impact on code

@antoinelamy

Copy link
Copy Markdown
ContributorAuthor

Non main thread calls warnings

I would also add a warning when .pin is called from a thread other than the main thread (https://github.com/layoutBox/PinLayout#pinlayouts-warnings).
Ex:
⚠️ PinLayout should be used only from the main thread. UIKit calls must be called from the app's main thread.

This warning could be disabled Pin.activeWarnings.mainThread.

From what I see there is already such warning in PinLayout+Warning.displayLayoutWarnings() and it already covers the case of autosizing as we enter the apply() function the same way regular layout does:

if !Thread.isMainThread {
warn("Layout must be executed from the Main Thread!")
}

@antoinelamy
antoinelamy marked this pull request as ready for review June 16, 2020 11:49
@antoinelamyantoinelamy changed the title WIP: Add automatic sizeThatFits computation for viewsAdd automatic sizeThatFits computation for viewsJun 16, 2020
@antoinelamy

Copy link
Copy Markdown
ContributorAuthor

Should be good to go now @lucdion, anything else comes to mind?

Comment threadSources/Impl/PinLayout+Layouting.swift Outdated
@lucdion

Copy link
Copy Markdown
Member

Two more things:

  1. Could you duplicate the sample that you had previously modified? At least it shows an example.
  2. You won't like that, but even if its an experimental feature. We would need a minimalist documentation that describes the feature and how to use it. I would add that new section below https://github.com/layoutBox/PinLayout#justify--align
    Thanks

@antoinelamy

Copy link
Copy Markdown
ContributorAuthor

@lucdion I thought you preferred to silent release this feature, my mistake. I added a proper sample that fetches a random text and a random sized image. That content is then layout in a container that uses autosizing to determine its proper size. I also included setting a scroll view content size to illustrate the fact that we should not call non PinLayout code in the layout closure passed to autoSizeThatFits. I also added some documentation in the Readme for it, not sure if it's good enough but I'll leave it to you for review.

@lucdionlucdion left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ready to go 🎉
Thanks @antoinelamy for this nice addition

@lucdion
lucdion merged commit 21e53ef into layoutBox:masterJun 30, 2020
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@antoinelamy@lucdion
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add automatic sizeThatFits computation for views - #216

Merged
lucdion merged 23 commits into
layoutBox:masterfrom
antoinelamy:feature/AutoSizing
Jun 30, 2020
Merged

Add automatic sizeThatFits computation for views#216
lucdion merged 23 commits into
layoutBox:masterfrom
antoinelamy:feature/AutoSizing

Conversation

@antoinelamy

Copy link
Copy Markdown
Contributor

Disclaimer

This is a preliminary PR for discussion purpose. The implemented functionality has barely been tested and is not guaranteed to be bug free.

Motivation

Implementing sizeThatFits(_ size: CGSize) as part of the manual layout process has always been cumbersome. You always end up writing the same code twice, a first time for the layout and the second time for sizing. Using PinLayout to compute the resulting size like showcased in the AdjustToContainer exemple is not recommended either because the view coordinates are modified during sizing. The sizeThatFits method documentation state clearly:

This method does not resize the receiver.

Proposal

Build an autosizing mechanism on top of PinLayout without modifying the view's coordinates in the process.

  • This PR add the AutoSizeCalculable interface that defines autosizing related functions and properties.
  • An internal flag (Pin.autoSizingInProgress) is also needed for the layout system to know if it should compute an additional rect including the margins.
  • Implementing sizeThatFits(_ size: CGSize) using automatic sizing requires the following:
    • Layout code is preferably located in a separate function than layoutSubviews() to be sure things like setting the content size on a scroll view are not executed during the sizing process. In the provided exemple, that function would be layout().
    • The sizeThatFits implementation is as simple as calling return autoSizeThatFits(size) { layout() } and even takes into account the outer margins (ie: the bottom margin applied to the last view on y axis)

Limitations

  • Sadly I don't see any way of adding this capability directly on Layoutable because of the need to define stored properties.
  • All layout related code must be done using PinLayout only, otherwise the views that uses another layout system would be ignored in the resulting computed size.

Discussion

I would very much like feedback on this PR discussing the concept and exposing the potential flaws if any. I think that if we can get this thing to work properly in every situation it would be a great addition to PinLayout.

@antoinelamy
antoinelamy marked this pull request as draft May 25, 2020 01:02

@lucdionlucdion left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Salut Antoine 🙂

I like the general idea of your PR.

Global variable Pin.autoSizingInProgress

The only thing that bothers me is Pin.autoSizingInProgress. I know that all layouts related code should be executed from the main thread, so, a well-programmed app should not have 2 views that are layouted simultaneously. So having a global variable that contains that state shouldn't be an issue. But still, it's a global variable.

I'm not sure what could be a nicer solution thought, or if there is such a solution. But here is some thought on it (maybe this will trigger other ideas on your side 🤞).

Solution 1: Add a layout context
Suppose we add a method pin(_ context: LayoutContext?) and we add a context parameter to the layoutClosure, this context could then be passed to all pin calls. Ex:

override func sizeThatFits(_ size: CGSize) -> CGSize {
return autoSizeThatFits(size) { (ctx: LayoutContext) in layout(ctx) }
}
private func layout(_ ctx: LayoutContext?) {
subview.pin(ctx).top().left(10).width(200);
}

Cons:

  • A little verbose and not as nice.
  • It's easy to forget to call pin(_ context: LayoutContext?) instead of .pin.

Solution 2: Check view's parents
Check in the view hierarchy if there is a parent view with an autoSizingRect value, in that case, autoSizing is in progress. For this to work, we need to set to nil the property autoSizingRect when leaving autoSizeThatFits(...).

Cons:

  • Need to scans all view's parents to detect that there is no autoSizing in progress 😞

Solution 3: Global variable
Keep the global variable 😕

Experimental feature

In all cases I would keep that feature as experimental, i.e. we don't document immediately until you have played with the feature and you are happy with the result. During that time, you could note anything that would be useful to eventually document.

Non main thread calls warnings

I would also add a warning when .pin is called from a thread other than the main thread (https://github.com/layoutBox/PinLayout#pinlayouts-warnings).
Ex:
⚠️ PinLayout should be used only from the main thread. UIKit calls must be called from the app's main thread.

This warning could be disabled Pin.activeWarnings.mainThread.

...

I will continue to think about this feauture, so I may add other comments later

Comment threadSources/Impl/PinLayout+Layouting.swift Outdated
Comment threadSources/Extensions/UIView+PinLayout.swift Outdated
Comment threadSources/Extensions/UIView+PinLayout.swift Outdated
}
}

public func autoSizeThatFits(_ size: CGSize, layoutClosure: () -> Void) -> CGSize {

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really would like to extract that function out of the UIView conformance extension but the only thing that prevent me to do so is the call to let adjustedRect = Coordinates<View>.adjustRectToDisplayScale(rect). I think it would make sense to add a displayScale property on the Layoutable protocol and perform that transformation before calling setRect. We really want the resulting rect to be the same in both layout and auto sizing.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have perform the changes to do this but I believe it would make more sense in a separate PR as it also fix an issue in the current production code.

@antoinelamy

antoinelamy commented Jun 5, 2020

Copy link
Copy Markdown
ContributorAuthor

I agree that a global feature flag is probably not the most elegant solution but at the same time it's the less invasive way to implement it. I started trying to implement it creating a Pin object for a different object type acting as a view proxy but the code was overly complex and I ended up having the same need for a state variable.

Solution 1
It would require all the existing layout code to be refactored to opt-in to the feature plus it loose a bit of its simplicity that makes PinLayout so appealing IMO and like you said it would be easy to forget passing in the context object during pin creation.

Solution 2
I fear that the performance would take a huge hit on that one.

Solution 3
Well, I think it's the less bad solution and layout code should not be called from non main thread anyway, setting bounds or position on a background thread is forbidden. It might be a problem for another unknown Layoutable type that could potentially be layouted on multiple threads. In that case, my advise would be to not make that type conform to AutoSizeable.

The API is a bit similar to UIView.animate that wraps a beginAnimation() / endAnimation() behind the scene and a global flag is used to know wether or not the change should be performed animated or not. The fact that the API uses a closure where the layout code is expected to be called makes it less error prone I think.

@lucdion

Copy link
Copy Markdown
Member

I agree @antoinelamy, that your solution is probably the best, even if there is one flaw, its the one that has a minimal impact on code

@antoinelamy

Copy link
Copy Markdown
ContributorAuthor

Non main thread calls warnings

I would also add a warning when .pin is called from a thread other than the main thread (https://github.com/layoutBox/PinLayout#pinlayouts-warnings).
Ex:
⚠️ PinLayout should be used only from the main thread. UIKit calls must be called from the app's main thread.

This warning could be disabled Pin.activeWarnings.mainThread.

From what I see there is already such warning in PinLayout+Warning.displayLayoutWarnings() and it already covers the case of autosizing as we enter the apply() function the same way regular layout does:

if !Thread.isMainThread {
warn("Layout must be executed from the Main Thread!")
}

@antoinelamy
antoinelamy marked this pull request as ready for review June 16, 2020 11:49
@antoinelamyantoinelamy changed the title WIP: Add automatic sizeThatFits computation for viewsAdd automatic sizeThatFits computation for viewsJun 16, 2020
@antoinelamy

Copy link
Copy Markdown
ContributorAuthor

Should be good to go now @lucdion, anything else comes to mind?

Comment threadSources/Impl/PinLayout+Layouting.swift Outdated
@lucdion

Copy link
Copy Markdown
Member

Two more things:

  1. Could you duplicate the sample that you had previously modified? At least it shows an example.
  2. You won't like that, but even if its an experimental feature. We would need a minimalist documentation that describes the feature and how to use it. I would add that new section below https://github.com/layoutBox/PinLayout#justify--align
    Thanks

@antoinelamy

Copy link
Copy Markdown
ContributorAuthor

@lucdion I thought you preferred to silent release this feature, my mistake. I added a proper sample that fetches a random text and a random sized image. That content is then layout in a container that uses autosizing to determine its proper size. I also included setting a scroll view content size to illustrate the fact that we should not call non PinLayout code in the layout closure passed to autoSizeThatFits. I also added some documentation in the Readme for it, not sure if it's good enough but I'll leave it to you for review.

@lucdionlucdion left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ready to go 🎉
Thanks @antoinelamy for this nice addition

@lucdion
lucdion merged commit 21e53ef into layoutBox:masterJun 30, 2020
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@antoinelamy@lucdion
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add automatic sizeThatFits computation for views - #216

Merged
lucdion merged 23 commits into
layoutBox:masterfrom
antoinelamy:feature/AutoSizing
Jun 30, 2020
Merged

Add automatic sizeThatFits computation for views#216
lucdion merged 23 commits into
layoutBox:masterfrom
antoinelamy:feature/AutoSizing

Conversation

@antoinelamy

Copy link
Copy Markdown
Contributor

Disclaimer

This is a preliminary PR for discussion purpose. The implemented functionality has barely been tested and is not guaranteed to be bug free.

Motivation

Implementing sizeThatFits(_ size: CGSize) as part of the manual layout process has always been cumbersome. You always end up writing the same code twice, a first time for the layout and the second time for sizing. Using PinLayout to compute the resulting size like showcased in the AdjustToContainer exemple is not recommended either because the view coordinates are modified during sizing. The sizeThatFits method documentation state clearly:

This method does not resize the receiver.

Proposal

Build an autosizing mechanism on top of PinLayout without modifying the view's coordinates in the process.

  • This PR add the AutoSizeCalculable interface that defines autosizing related functions and properties.
  • An internal flag (Pin.autoSizingInProgress) is also needed for the layout system to know if it should compute an additional rect including the margins.
  • Implementing sizeThatFits(_ size: CGSize) using automatic sizing requires the following:
    • Layout code is preferably located in a separate function than layoutSubviews() to be sure things like setting the content size on a scroll view are not executed during the sizing process. In the provided exemple, that function would be layout().
    • The sizeThatFits implementation is as simple as calling return autoSizeThatFits(size) { layout() } and even takes into account the outer margins (ie: the bottom margin applied to the last view on y axis)

Limitations

  • Sadly I don't see any way of adding this capability directly on Layoutable because of the need to define stored properties.
  • All layout related code must be done using PinLayout only, otherwise the views that uses another layout system would be ignored in the resulting computed size.

Discussion

I would very much like feedback on this PR discussing the concept and exposing the potential flaws if any. I think that if we can get this thing to work properly in every situation it would be a great addition to PinLayout.

@antoinelamy
antoinelamy marked this pull request as draft May 25, 2020 01:02

@lucdionlucdion left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Salut Antoine 🙂

I like the general idea of your PR.

Global variable Pin.autoSizingInProgress

The only thing that bothers me is Pin.autoSizingInProgress. I know that all layouts related code should be executed from the main thread, so, a well-programmed app should not have 2 views that are layouted simultaneously. So having a global variable that contains that state shouldn't be an issue. But still, it's a global variable.

I'm not sure what could be a nicer solution thought, or if there is such a solution. But here is some thought on it (maybe this will trigger other ideas on your side 🤞).

Solution 1: Add a layout context
Suppose we add a method pin(_ context: LayoutContext?) and we add a context parameter to the layoutClosure, this context could then be passed to all pin calls. Ex:

override func sizeThatFits(_ size: CGSize) -> CGSize {
return autoSizeThatFits(size) { (ctx: LayoutContext) in layout(ctx) }
}
private func layout(_ ctx: LayoutContext?) {
subview.pin(ctx).top().left(10).width(200);
}

Cons:

  • A little verbose and not as nice.
  • It's easy to forget to call pin(_ context: LayoutContext?) instead of .pin.

Solution 2: Check view's parents
Check in the view hierarchy if there is a parent view with an autoSizingRect value, in that case, autoSizing is in progress. For this to work, we need to set to nil the property autoSizingRect when leaving autoSizeThatFits(...).

Cons:

  • Need to scans all view's parents to detect that there is no autoSizing in progress 😞

Solution 3: Global variable
Keep the global variable 😕

Experimental feature

In all cases I would keep that feature as experimental, i.e. we don't document immediately until you have played with the feature and you are happy with the result. During that time, you could note anything that would be useful to eventually document.

Non main thread calls warnings

I would also add a warning when .pin is called from a thread other than the main thread (https://github.com/layoutBox/PinLayout#pinlayouts-warnings).
Ex:
⚠️ PinLayout should be used only from the main thread. UIKit calls must be called from the app's main thread.

This warning could be disabled Pin.activeWarnings.mainThread.

...

I will continue to think about this feauture, so I may add other comments later

Comment threadSources/Impl/PinLayout+Layouting.swift Outdated
Comment threadSources/Extensions/UIView+PinLayout.swift Outdated
Comment threadSources/Extensions/UIView+PinLayout.swift Outdated
}
}

public func autoSizeThatFits(_ size: CGSize, layoutClosure: () -> Void) -> CGSize {

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really would like to extract that function out of the UIView conformance extension but the only thing that prevent me to do so is the call to let adjustedRect = Coordinates<View>.adjustRectToDisplayScale(rect). I think it would make sense to add a displayScale property on the Layoutable protocol and perform that transformation before calling setRect. We really want the resulting rect to be the same in both layout and auto sizing.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have perform the changes to do this but I believe it would make more sense in a separate PR as it also fix an issue in the current production code.

@antoinelamy

antoinelamy commented Jun 5, 2020

Copy link
Copy Markdown
ContributorAuthor

I agree that a global feature flag is probably not the most elegant solution but at the same time it's the less invasive way to implement it. I started trying to implement it creating a Pin object for a different object type acting as a view proxy but the code was overly complex and I ended up having the same need for a state variable.

Solution 1
It would require all the existing layout code to be refactored to opt-in to the feature plus it loose a bit of its simplicity that makes PinLayout so appealing IMO and like you said it would be easy to forget passing in the context object during pin creation.

Solution 2
I fear that the performance would take a huge hit on that one.

Solution 3
Well, I think it's the less bad solution and layout code should not be called from non main thread anyway, setting bounds or position on a background thread is forbidden. It might be a problem for another unknown Layoutable type that could potentially be layouted on multiple threads. In that case, my advise would be to not make that type conform to AutoSizeable.

The API is a bit similar to UIView.animate that wraps a beginAnimation() / endAnimation() behind the scene and a global flag is used to know wether or not the change should be performed animated or not. The fact that the API uses a closure where the layout code is expected to be called makes it less error prone I think.

@lucdion

Copy link
Copy Markdown
Member

I agree @antoinelamy, that your solution is probably the best, even if there is one flaw, its the one that has a minimal impact on code

@antoinelamy

Copy link
Copy Markdown
ContributorAuthor

Non main thread calls warnings

I would also add a warning when .pin is called from a thread other than the main thread (https://github.com/layoutBox/PinLayout#pinlayouts-warnings).
Ex:
⚠️ PinLayout should be used only from the main thread. UIKit calls must be called from the app's main thread.

This warning could be disabled Pin.activeWarnings.mainThread.

From what I see there is already such warning in PinLayout+Warning.displayLayoutWarnings() and it already covers the case of autosizing as we enter the apply() function the same way regular layout does:

if !Thread.isMainThread {
warn("Layout must be executed from the Main Thread!")
}

@antoinelamy
antoinelamy marked this pull request as ready for review June 16, 2020 11:49
@antoinelamyantoinelamy changed the title WIP: Add automatic sizeThatFits computation for viewsAdd automatic sizeThatFits computation for viewsJun 16, 2020
@antoinelamy

Copy link
Copy Markdown
ContributorAuthor

Should be good to go now @lucdion, anything else comes to mind?

Comment threadSources/Impl/PinLayout+Layouting.swift Outdated
@lucdion

Copy link
Copy Markdown
Member

Two more things:

  1. Could you duplicate the sample that you had previously modified? At least it shows an example.
  2. You won't like that, but even if its an experimental feature. We would need a minimalist documentation that describes the feature and how to use it. I would add that new section below https://github.com/layoutBox/PinLayout#justify--align
    Thanks

@antoinelamy

Copy link
Copy Markdown
ContributorAuthor

@lucdion I thought you preferred to silent release this feature, my mistake. I added a proper sample that fetches a random text and a random sized image. That content is then layout in a container that uses autosizing to determine its proper size. I also included setting a scroll view content size to illustrate the fact that we should not call non PinLayout code in the layout closure passed to autoSizeThatFits. I also added some documentation in the Readme for it, not sure if it's good enough but I'll leave it to you for review.

@lucdionlucdion left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ready to go 🎉
Thanks @antoinelamy for this nice addition

@lucdion
lucdion merged commit 21e53ef into layoutBox:masterJun 30, 2020
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@antoinelamy@lucdion
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Add automatic sizeThatFits computation for views - #216

Merged
lucdion merged 23 commits into
layoutBox:masterfrom
antoinelamy:feature/AutoSizing
Jun 30, 2020
Merged

Add automatic sizeThatFits computation for views#216
lucdion merged 23 commits into
layoutBox:masterfrom
antoinelamy:feature/AutoSizing

Conversation

@antoinelamy

Copy link
Copy Markdown
Contributor

Disclaimer

This is a preliminary PR for discussion purpose. The implemented functionality has barely been tested and is not guaranteed to be bug free.

Motivation

Implementing sizeThatFits(_ size: CGSize) as part of the manual layout process has always been cumbersome. You always end up writing the same code twice, a first time for the layout and the second time for sizing. Using PinLayout to compute the resulting size like showcased in the AdjustToContainer exemple is not recommended either because the view coordinates are modified during sizing. The sizeThatFits method documentation state clearly:

This method does not resize the receiver.

Proposal

Build an autosizing mechanism on top of PinLayout without modifying the view's coordinates in the process.

  • This PR add the AutoSizeCalculable interface that defines autosizing related functions and properties.
  • An internal flag (Pin.autoSizingInProgress) is also needed for the layout system to know if it should compute an additional rect including the margins.
  • Implementing sizeThatFits(_ size: CGSize) using automatic sizing requires the following:
    • Layout code is preferably located in a separate function than layoutSubviews() to be sure things like setting the content size on a scroll view are not executed during the sizing process. In the provided exemple, that function would be layout().
    • The sizeThatFits implementation is as simple as calling return autoSizeThatFits(size) { layout() } and even takes into account the outer margins (ie: the bottom margin applied to the last view on y axis)

Limitations

  • Sadly I don't see any way of adding this capability directly on Layoutable because of the need to define stored properties.
  • All layout related code must be done using PinLayout only, otherwise the views that uses another layout system would be ignored in the resulting computed size.

Discussion

I would very much like feedback on this PR discussing the concept and exposing the potential flaws if any. I think that if we can get this thing to work properly in every situation it would be a great addition to PinLayout.

@antoinelamy
antoinelamy marked this pull request as draft May 25, 2020 01:02

@lucdionlucdion left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Salut Antoine 🙂

I like the general idea of your PR.

Global variable Pin.autoSizingInProgress

The only thing that bothers me is Pin.autoSizingInProgress. I know that all layouts related code should be executed from the main thread, so, a well-programmed app should not have 2 views that are layouted simultaneously. So having a global variable that contains that state shouldn't be an issue. But still, it's a global variable.

I'm not sure what could be a nicer solution thought, or if there is such a solution. But here is some thought on it (maybe this will trigger other ideas on your side 🤞).

Solution 1: Add a layout context
Suppose we add a method pin(_ context: LayoutContext?) and we add a context parameter to the layoutClosure, this context could then be passed to all pin calls. Ex:

override func sizeThatFits(_ size: CGSize) -> CGSize {
return autoSizeThatFits(size) { (ctx: LayoutContext) in layout(ctx) }
}
private func layout(_ ctx: LayoutContext?) {
subview.pin(ctx).top().left(10).width(200);
}

Cons:

  • A little verbose and not as nice.
  • It's easy to forget to call pin(_ context: LayoutContext?) instead of .pin.

Solution 2: Check view's parents
Check in the view hierarchy if there is a parent view with an autoSizingRect value, in that case, autoSizing is in progress. For this to work, we need to set to nil the property autoSizingRect when leaving autoSizeThatFits(...).

Cons:

  • Need to scans all view's parents to detect that there is no autoSizing in progress 😞

Solution 3: Global variable
Keep the global variable 😕

Experimental feature

In all cases I would keep that feature as experimental, i.e. we don't document immediately until you have played with the feature and you are happy with the result. During that time, you could note anything that would be useful to eventually document.

Non main thread calls warnings

I would also add a warning when .pin is called from a thread other than the main thread (https://github.com/layoutBox/PinLayout#pinlayouts-warnings).
Ex:
⚠️ PinLayout should be used only from the main thread. UIKit calls must be called from the app's main thread.

This warning could be disabled Pin.activeWarnings.mainThread.

...

I will continue to think about this feauture, so I may add other comments later

Comment threadSources/Impl/PinLayout+Layouting.swift Outdated
Comment threadSources/Extensions/UIView+PinLayout.swift Outdated
Comment threadSources/Extensions/UIView+PinLayout.swift Outdated
}
}

public func autoSizeThatFits(_ size: CGSize, layoutClosure: () -> Void) -> CGSize {

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really would like to extract that function out of the UIView conformance extension but the only thing that prevent me to do so is the call to let adjustedRect = Coordinates<View>.adjustRectToDisplayScale(rect). I think it would make sense to add a displayScale property on the Layoutable protocol and perform that transformation before calling setRect. We really want the resulting rect to be the same in both layout and auto sizing.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have perform the changes to do this but I believe it would make more sense in a separate PR as it also fix an issue in the current production code.

@antoinelamy

antoinelamy commented Jun 5, 2020

Copy link
Copy Markdown
ContributorAuthor

I agree that a global feature flag is probably not the most elegant solution but at the same time it's the less invasive way to implement it. I started trying to implement it creating a Pin object for a different object type acting as a view proxy but the code was overly complex and I ended up having the same need for a state variable.

Solution 1
It would require all the existing layout code to be refactored to opt-in to the feature plus it loose a bit of its simplicity that makes PinLayout so appealing IMO and like you said it would be easy to forget passing in the context object during pin creation.

Solution 2
I fear that the performance would take a huge hit on that one.

Solution 3
Well, I think it's the less bad solution and layout code should not be called from non main thread anyway, setting bounds or position on a background thread is forbidden. It might be a problem for another unknown Layoutable type that could potentially be layouted on multiple threads. In that case, my advise would be to not make that type conform to AutoSizeable.

The API is a bit similar to UIView.animate that wraps a beginAnimation() / endAnimation() behind the scene and a global flag is used to know wether or not the change should be performed animated or not. The fact that the API uses a closure where the layout code is expected to be called makes it less error prone I think.

@lucdion

Copy link
Copy Markdown
Member

I agree @antoinelamy, that your solution is probably the best, even if there is one flaw, its the one that has a minimal impact on code

@antoinelamy

Copy link
Copy Markdown
ContributorAuthor

Non main thread calls warnings

I would also add a warning when .pin is called from a thread other than the main thread (https://github.com/layoutBox/PinLayout#pinlayouts-warnings).
Ex:
⚠️ PinLayout should be used only from the main thread. UIKit calls must be called from the app's main thread.

This warning could be disabled Pin.activeWarnings.mainThread.

From what I see there is already such warning in PinLayout+Warning.displayLayoutWarnings() and it already covers the case of autosizing as we enter the apply() function the same way regular layout does:

if !Thread.isMainThread {
warn("Layout must be executed from the Main Thread!")
}

@antoinelamy
antoinelamy marked this pull request as ready for review June 16, 2020 11:49
@antoinelamyantoinelamy changed the title WIP: Add automatic sizeThatFits computation for viewsAdd automatic sizeThatFits computation for viewsJun 16, 2020
@antoinelamy

Copy link
Copy Markdown
ContributorAuthor

Should be good to go now @lucdion, anything else comes to mind?

Comment threadSources/Impl/PinLayout+Layouting.swift Outdated
@lucdion

Copy link
Copy Markdown
Member

Two more things:

  1. Could you duplicate the sample that you had previously modified? At least it shows an example.
  2. You won't like that, but even if its an experimental feature. We would need a minimalist documentation that describes the feature and how to use it. I would add that new section below https://github.com/layoutBox/PinLayout#justify--align
    Thanks

@antoinelamy

Copy link
Copy Markdown
ContributorAuthor

@lucdion I thought you preferred to silent release this feature, my mistake. I added a proper sample that fetches a random text and a random sized image. That content is then layout in a container that uses autosizing to determine its proper size. I also included setting a scroll view content size to illustrate the fact that we should not call non PinLayout code in the layout closure passed to autoSizeThatFits. I also added some documentation in the Readme for it, not sure if it's good enough but I'll leave it to you for review.

@lucdionlucdion left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ready to go 🎉
Thanks @antoinelamy for this nice addition

@lucdion
lucdion merged commit 21e53ef into layoutBox:masterJun 30, 2020
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@antoinelamy@lucdion
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add automatic sizeThatFits computation for views - #216

Merged
lucdion merged 23 commits into
layoutBox:masterfrom
antoinelamy:feature/AutoSizing
Jun 30, 2020
Merged

Add automatic sizeThatFits computation for views#216
lucdion merged 23 commits into
layoutBox:masterfrom
antoinelamy:feature/AutoSizing

Conversation

@antoinelamy

Copy link
Copy Markdown
Contributor

Disclaimer

This is a preliminary PR for discussion purpose. The implemented functionality has barely been tested and is not guaranteed to be bug free.

Motivation

Implementing sizeThatFits(_ size: CGSize) as part of the manual layout process has always been cumbersome. You always end up writing the same code twice, a first time for the layout and the second time for sizing. Using PinLayout to compute the resulting size like showcased in the AdjustToContainer exemple is not recommended either because the view coordinates are modified during sizing. The sizeThatFits method documentation state clearly:

This method does not resize the receiver.

Proposal

Build an autosizing mechanism on top of PinLayout without modifying the view's coordinates in the process.

  • This PR add the AutoSizeCalculable interface that defines autosizing related functions and properties.
  • An internal flag (Pin.autoSizingInProgress) is also needed for the layout system to know if it should compute an additional rect including the margins.
  • Implementing sizeThatFits(_ size: CGSize) using automatic sizing requires the following:
    • Layout code is preferably located in a separate function than layoutSubviews() to be sure things like setting the content size on a scroll view are not executed during the sizing process. In the provided exemple, that function would be layout().
    • The sizeThatFits implementation is as simple as calling return autoSizeThatFits(size) { layout() } and even takes into account the outer margins (ie: the bottom margin applied to the last view on y axis)

Limitations

  • Sadly I don't see any way of adding this capability directly on Layoutable because of the need to define stored properties.
  • All layout related code must be done using PinLayout only, otherwise the views that uses another layout system would be ignored in the resulting computed size.

Discussion

I would very much like feedback on this PR discussing the concept and exposing the potential flaws if any. I think that if we can get this thing to work properly in every situation it would be a great addition to PinLayout.

@antoinelamy
antoinelamy marked this pull request as draft May 25, 2020 01:02

@lucdionlucdion left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Salut Antoine 🙂

I like the general idea of your PR.

Global variable Pin.autoSizingInProgress

The only thing that bothers me is Pin.autoSizingInProgress. I know that all layouts related code should be executed from the main thread, so, a well-programmed app should not have 2 views that are layouted simultaneously. So having a global variable that contains that state shouldn't be an issue. But still, it's a global variable.

I'm not sure what could be a nicer solution thought, or if there is such a solution. But here is some thought on it (maybe this will trigger other ideas on your side 🤞).

Solution 1: Add a layout context
Suppose we add a method pin(_ context: LayoutContext?) and we add a context parameter to the layoutClosure, this context could then be passed to all pin calls. Ex:

override func sizeThatFits(_ size: CGSize) -> CGSize {
return autoSizeThatFits(size) { (ctx: LayoutContext) in layout(ctx) }
}
private func layout(_ ctx: LayoutContext?) {
subview.pin(ctx).top().left(10).width(200);
}

Cons:

  • A little verbose and not as nice.
  • It's easy to forget to call pin(_ context: LayoutContext?) instead of .pin.

Solution 2: Check view's parents
Check in the view hierarchy if there is a parent view with an autoSizingRect value, in that case, autoSizing is in progress. For this to work, we need to set to nil the property autoSizingRect when leaving autoSizeThatFits(...).

Cons:

  • Need to scans all view's parents to detect that there is no autoSizing in progress 😞

Solution 3: Global variable
Keep the global variable 😕

Experimental feature

In all cases I would keep that feature as experimental, i.e. we don't document immediately until you have played with the feature and you are happy with the result. During that time, you could note anything that would be useful to eventually document.

Non main thread calls warnings

I would also add a warning when .pin is called from a thread other than the main thread (https://github.com/layoutBox/PinLayout#pinlayouts-warnings).
Ex:
⚠️ PinLayout should be used only from the main thread. UIKit calls must be called from the app's main thread.

This warning could be disabled Pin.activeWarnings.mainThread.

...

I will continue to think about this feauture, so I may add other comments later

Comment threadSources/Impl/PinLayout+Layouting.swift Outdated
Comment threadSources/Extensions/UIView+PinLayout.swift Outdated
Comment threadSources/Extensions/UIView+PinLayout.swift Outdated
}
}

public func autoSizeThatFits(_ size: CGSize, layoutClosure: () -> Void) -> CGSize {

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really would like to extract that function out of the UIView conformance extension but the only thing that prevent me to do so is the call to let adjustedRect = Coordinates<View>.adjustRectToDisplayScale(rect). I think it would make sense to add a displayScale property on the Layoutable protocol and perform that transformation before calling setRect. We really want the resulting rect to be the same in both layout and auto sizing.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have perform the changes to do this but I believe it would make more sense in a separate PR as it also fix an issue in the current production code.

@antoinelamy

antoinelamy commented Jun 5, 2020

Copy link
Copy Markdown
ContributorAuthor

I agree that a global feature flag is probably not the most elegant solution but at the same time it's the less invasive way to implement it. I started trying to implement it creating a Pin object for a different object type acting as a view proxy but the code was overly complex and I ended up having the same need for a state variable.

Solution 1
It would require all the existing layout code to be refactored to opt-in to the feature plus it loose a bit of its simplicity that makes PinLayout so appealing IMO and like you said it would be easy to forget passing in the context object during pin creation.

Solution 2
I fear that the performance would take a huge hit on that one.

Solution 3
Well, I think it's the less bad solution and layout code should not be called from non main thread anyway, setting bounds or position on a background thread is forbidden. It might be a problem for another unknown Layoutable type that could potentially be layouted on multiple threads. In that case, my advise would be to not make that type conform to AutoSizeable.

The API is a bit similar to UIView.animate that wraps a beginAnimation() / endAnimation() behind the scene and a global flag is used to know wether or not the change should be performed animated or not. The fact that the API uses a closure where the layout code is expected to be called makes it less error prone I think.

@lucdion

Copy link
Copy Markdown
Member

I agree @antoinelamy, that your solution is probably the best, even if there is one flaw, its the one that has a minimal impact on code

@antoinelamy

Copy link
Copy Markdown
ContributorAuthor

Non main thread calls warnings

I would also add a warning when .pin is called from a thread other than the main thread (https://github.com/layoutBox/PinLayout#pinlayouts-warnings).
Ex:
⚠️ PinLayout should be used only from the main thread. UIKit calls must be called from the app's main thread.

This warning could be disabled Pin.activeWarnings.mainThread.

From what I see there is already such warning in PinLayout+Warning.displayLayoutWarnings() and it already covers the case of autosizing as we enter the apply() function the same way regular layout does:

if !Thread.isMainThread {
warn("Layout must be executed from the Main Thread!")
}

@antoinelamy
antoinelamy marked this pull request as ready for review June 16, 2020 11:49
@antoinelamyantoinelamy changed the title WIP: Add automatic sizeThatFits computation for viewsAdd automatic sizeThatFits computation for viewsJun 16, 2020
@antoinelamy

Copy link
Copy Markdown
ContributorAuthor

Should be good to go now @lucdion, anything else comes to mind?

Comment threadSources/Impl/PinLayout+Layouting.swift Outdated
@lucdion

Copy link
Copy Markdown
Member

Two more things:

  1. Could you duplicate the sample that you had previously modified? At least it shows an example.
  2. You won't like that, but even if its an experimental feature. We would need a minimalist documentation that describes the feature and how to use it. I would add that new section below https://github.com/layoutBox/PinLayout#justify--align
    Thanks

@antoinelamy

Copy link
Copy Markdown
ContributorAuthor

@lucdion I thought you preferred to silent release this feature, my mistake. I added a proper sample that fetches a random text and a random sized image. That content is then layout in a container that uses autosizing to determine its proper size. I also included setting a scroll view content size to illustrate the fact that we should not call non PinLayout code in the layout closure passed to autoSizeThatFits. I also added some documentation in the Readme for it, not sure if it's good enough but I'll leave it to you for review.

@lucdionlucdion left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ready to go 🎉
Thanks @antoinelamy for this nice addition

@lucdion
lucdion merged commit 21e53ef into layoutBox:masterJun 30, 2020
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@antoinelamy@lucdion
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add automatic sizeThatFits computation for views - #216

Merged
lucdion merged 23 commits into
layoutBox:masterfrom
antoinelamy:feature/AutoSizing
Jun 30, 2020
Merged

Add automatic sizeThatFits computation for views#216
lucdion merged 23 commits into
layoutBox:masterfrom
antoinelamy:feature/AutoSizing

Conversation

@antoinelamy

Copy link
Copy Markdown
Contributor

Disclaimer

This is a preliminary PR for discussion purpose. The implemented functionality has barely been tested and is not guaranteed to be bug free.

Motivation

Implementing sizeThatFits(_ size: CGSize) as part of the manual layout process has always been cumbersome. You always end up writing the same code twice, a first time for the layout and the second time for sizing. Using PinLayout to compute the resulting size like showcased in the AdjustToContainer exemple is not recommended either because the view coordinates are modified during sizing. The sizeThatFits method documentation state clearly:

This method does not resize the receiver.

Proposal

Build an autosizing mechanism on top of PinLayout without modifying the view's coordinates in the process.

  • This PR add the AutoSizeCalculable interface that defines autosizing related functions and properties.
  • An internal flag (Pin.autoSizingInProgress) is also needed for the layout system to know if it should compute an additional rect including the margins.
  • Implementing sizeThatFits(_ size: CGSize) using automatic sizing requires the following:
    • Layout code is preferably located in a separate function than layoutSubviews() to be sure things like setting the content size on a scroll view are not executed during the sizing process. In the provided exemple, that function would be layout().
    • The sizeThatFits implementation is as simple as calling return autoSizeThatFits(size) { layout() } and even takes into account the outer margins (ie: the bottom margin applied to the last view on y axis)

Limitations

  • Sadly I don't see any way of adding this capability directly on Layoutable because of the need to define stored properties.
  • All layout related code must be done using PinLayout only, otherwise the views that uses another layout system would be ignored in the resulting computed size.

Discussion

I would very much like feedback on this PR discussing the concept and exposing the potential flaws if any. I think that if we can get this thing to work properly in every situation it would be a great addition to PinLayout.

@antoinelamy
antoinelamy marked this pull request as draft May 25, 2020 01:02

@lucdionlucdion left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Salut Antoine 🙂

I like the general idea of your PR.

Global variable Pin.autoSizingInProgress

The only thing that bothers me is Pin.autoSizingInProgress. I know that all layouts related code should be executed from the main thread, so, a well-programmed app should not have 2 views that are layouted simultaneously. So having a global variable that contains that state shouldn't be an issue. But still, it's a global variable.

I'm not sure what could be a nicer solution thought, or if there is such a solution. But here is some thought on it (maybe this will trigger other ideas on your side 🤞).

Solution 1: Add a layout context
Suppose we add a method pin(_ context: LayoutContext?) and we add a context parameter to the layoutClosure, this context could then be passed to all pin calls. Ex:

override func sizeThatFits(_ size: CGSize) -> CGSize {
return autoSizeThatFits(size) { (ctx: LayoutContext) in layout(ctx) }
}
private func layout(_ ctx: LayoutContext?) {
subview.pin(ctx).top().left(10).width(200);
}

Cons:

  • A little verbose and not as nice.
  • It's easy to forget to call pin(_ context: LayoutContext?) instead of .pin.

Solution 2: Check view's parents
Check in the view hierarchy if there is a parent view with an autoSizingRect value, in that case, autoSizing is in progress. For this to work, we need to set to nil the property autoSizingRect when leaving autoSizeThatFits(...).

Cons:

  • Need to scans all view's parents to detect that there is no autoSizing in progress 😞

Solution 3: Global variable
Keep the global variable 😕

Experimental feature

In all cases I would keep that feature as experimental, i.e. we don't document immediately until you have played with the feature and you are happy with the result. During that time, you could note anything that would be useful to eventually document.

Non main thread calls warnings

I would also add a warning when .pin is called from a thread other than the main thread (https://github.com/layoutBox/PinLayout#pinlayouts-warnings).
Ex:
⚠️ PinLayout should be used only from the main thread. UIKit calls must be called from the app's main thread.

This warning could be disabled Pin.activeWarnings.mainThread.

...

I will continue to think about this feauture, so I may add other comments later

Comment threadSources/Impl/PinLayout+Layouting.swift Outdated
Comment threadSources/Extensions/UIView+PinLayout.swift Outdated
Comment threadSources/Extensions/UIView+PinLayout.swift Outdated
}
}

public func autoSizeThatFits(_ size: CGSize, layoutClosure: () -> Void) -> CGSize {

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really would like to extract that function out of the UIView conformance extension but the only thing that prevent me to do so is the call to let adjustedRect = Coordinates<View>.adjustRectToDisplayScale(rect). I think it would make sense to add a displayScale property on the Layoutable protocol and perform that transformation before calling setRect. We really want the resulting rect to be the same in both layout and auto sizing.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have perform the changes to do this but I believe it would make more sense in a separate PR as it also fix an issue in the current production code.

@antoinelamy

antoinelamy commented Jun 5, 2020

Copy link
Copy Markdown
ContributorAuthor

I agree that a global feature flag is probably not the most elegant solution but at the same time it's the less invasive way to implement it. I started trying to implement it creating a Pin object for a different object type acting as a view proxy but the code was overly complex and I ended up having the same need for a state variable.

Solution 1
It would require all the existing layout code to be refactored to opt-in to the feature plus it loose a bit of its simplicity that makes PinLayout so appealing IMO and like you said it would be easy to forget passing in the context object during pin creation.

Solution 2
I fear that the performance would take a huge hit on that one.

Solution 3
Well, I think it's the less bad solution and layout code should not be called from non main thread anyway, setting bounds or position on a background thread is forbidden. It might be a problem for another unknown Layoutable type that could potentially be layouted on multiple threads. In that case, my advise would be to not make that type conform to AutoSizeable.

The API is a bit similar to UIView.animate that wraps a beginAnimation() / endAnimation() behind the scene and a global flag is used to know wether or not the change should be performed animated or not. The fact that the API uses a closure where the layout code is expected to be called makes it less error prone I think.

@lucdion

Copy link
Copy Markdown
Member

I agree @antoinelamy, that your solution is probably the best, even if there is one flaw, its the one that has a minimal impact on code

@antoinelamy

Copy link
Copy Markdown
ContributorAuthor

Non main thread calls warnings

I would also add a warning when .pin is called from a thread other than the main thread (https://github.com/layoutBox/PinLayout#pinlayouts-warnings).
Ex:
⚠️ PinLayout should be used only from the main thread. UIKit calls must be called from the app's main thread.

This warning could be disabled Pin.activeWarnings.mainThread.

From what I see there is already such warning in PinLayout+Warning.displayLayoutWarnings() and it already covers the case of autosizing as we enter the apply() function the same way regular layout does:

if !Thread.isMainThread {
warn("Layout must be executed from the Main Thread!")
}

@antoinelamy
antoinelamy marked this pull request as ready for review June 16, 2020 11:49
@antoinelamyantoinelamy changed the title WIP: Add automatic sizeThatFits computation for viewsAdd automatic sizeThatFits computation for viewsJun 16, 2020
@antoinelamy

Copy link
Copy Markdown
ContributorAuthor

Should be good to go now @lucdion, anything else comes to mind?

Comment threadSources/Impl/PinLayout+Layouting.swift Outdated
@lucdion

Copy link
Copy Markdown
Member

Two more things:

  1. Could you duplicate the sample that you had previously modified? At least it shows an example.
  2. You won't like that, but even if its an experimental feature. We would need a minimalist documentation that describes the feature and how to use it. I would add that new section below https://github.com/layoutBox/PinLayout#justify--align
    Thanks

@antoinelamy

Copy link
Copy Markdown
ContributorAuthor

@lucdion I thought you preferred to silent release this feature, my mistake. I added a proper sample that fetches a random text and a random sized image. That content is then layout in a container that uses autosizing to determine its proper size. I also included setting a scroll view content size to illustrate the fact that we should not call non PinLayout code in the layout closure passed to autoSizeThatFits. I also added some documentation in the Readme for it, not sure if it's good enough but I'll leave it to you for review.

@lucdionlucdion left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ready to go 🎉
Thanks @antoinelamy for this nice addition

@lucdion
lucdion merged commit 21e53ef into layoutBox:masterJun 30, 2020
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@antoinelamy@lucdion
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Add automatic sizeThatFits computation for views - #216

Merged
lucdion merged 23 commits into
layoutBox:masterfrom
antoinelamy:feature/AutoSizing
Jun 30, 2020
Merged

Add automatic sizeThatFits computation for views#216
lucdion merged 23 commits into
layoutBox:masterfrom
antoinelamy:feature/AutoSizing

Conversation

@antoinelamy

Copy link
Copy Markdown
Contributor

Disclaimer

This is a preliminary PR for discussion purpose. The implemented functionality has barely been tested and is not guaranteed to be bug free.

Motivation

Implementing sizeThatFits(_ size: CGSize) as part of the manual layout process has always been cumbersome. You always end up writing the same code twice, a first time for the layout and the second time for sizing. Using PinLayout to compute the resulting size like showcased in the AdjustToContainer exemple is not recommended either because the view coordinates are modified during sizing. The sizeThatFits method documentation state clearly:

This method does not resize the receiver.

Proposal

Build an autosizing mechanism on top of PinLayout without modifying the view's coordinates in the process.

  • This PR add the AutoSizeCalculable interface that defines autosizing related functions and properties.
  • An internal flag (Pin.autoSizingInProgress) is also needed for the layout system to know if it should compute an additional rect including the margins.
  • Implementing sizeThatFits(_ size: CGSize) using automatic sizing requires the following:
    • Layout code is preferably located in a separate function than layoutSubviews() to be sure things like setting the content size on a scroll view are not executed during the sizing process. In the provided exemple, that function would be layout().
    • The sizeThatFits implementation is as simple as calling return autoSizeThatFits(size) { layout() } and even takes into account the outer margins (ie: the bottom margin applied to the last view on y axis)

Limitations

  • Sadly I don't see any way of adding this capability directly on Layoutable because of the need to define stored properties.
  • All layout related code must be done using PinLayout only, otherwise the views that uses another layout system would be ignored in the resulting computed size.

Discussion

I would very much like feedback on this PR discussing the concept and exposing the potential flaws if any. I think that if we can get this thing to work properly in every situation it would be a great addition to PinLayout.

@antoinelamy
antoinelamy marked this pull request as draft May 25, 2020 01:02

@lucdionlucdion left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Salut Antoine 🙂

I like the general idea of your PR.

Global variable Pin.autoSizingInProgress

The only thing that bothers me is Pin.autoSizingInProgress. I know that all layouts related code should be executed from the main thread, so, a well-programmed app should not have 2 views that are layouted simultaneously. So having a global variable that contains that state shouldn't be an issue. But still, it's a global variable.

I'm not sure what could be a nicer solution thought, or if there is such a solution. But here is some thought on it (maybe this will trigger other ideas on your side 🤞).

Solution 1: Add a layout context
Suppose we add a method pin(_ context: LayoutContext?) and we add a context parameter to the layoutClosure, this context could then be passed to all pin calls. Ex:

override func sizeThatFits(_ size: CGSize) -> CGSize {
return autoSizeThatFits(size) { (ctx: LayoutContext) in layout(ctx) }
}
private func layout(_ ctx: LayoutContext?) {
subview.pin(ctx).top().left(10).width(200);
}

Cons:

  • A little verbose and not as nice.
  • It's easy to forget to call pin(_ context: LayoutContext?) instead of .pin.

Solution 2: Check view's parents
Check in the view hierarchy if there is a parent view with an autoSizingRect value, in that case, autoSizing is in progress. For this to work, we need to set to nil the property autoSizingRect when leaving autoSizeThatFits(...).

Cons:

  • Need to scans all view's parents to detect that there is no autoSizing in progress 😞

Solution 3: Global variable
Keep the global variable 😕

Experimental feature

In all cases I would keep that feature as experimental, i.e. we don't document immediately until you have played with the feature and you are happy with the result. During that time, you could note anything that would be useful to eventually document.

Non main thread calls warnings

I would also add a warning when .pin is called from a thread other than the main thread (https://github.com/layoutBox/PinLayout#pinlayouts-warnings).
Ex:
⚠️ PinLayout should be used only from the main thread. UIKit calls must be called from the app's main thread.

This warning could be disabled Pin.activeWarnings.mainThread.

...

I will continue to think about this feauture, so I may add other comments later

Comment threadSources/Impl/PinLayout+Layouting.swift Outdated
Comment threadSources/Extensions/UIView+PinLayout.swift Outdated
Comment threadSources/Extensions/UIView+PinLayout.swift Outdated
}
}

public func autoSizeThatFits(_ size: CGSize, layoutClosure: () -> Void) -> CGSize {

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I really would like to extract that function out of the UIView conformance extension but the only thing that prevent me to do so is the call to let adjustedRect = Coordinates<View>.adjustRectToDisplayScale(rect). I think it would make sense to add a displayScale property on the Layoutable protocol and perform that transformation before calling setRect. We really want the resulting rect to be the same in both layout and auto sizing.

Copy link
Copy Markdown
ContributorAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have perform the changes to do this but I believe it would make more sense in a separate PR as it also fix an issue in the current production code.

@antoinelamy

antoinelamy commented Jun 5, 2020

Copy link
Copy Markdown
ContributorAuthor

I agree that a global feature flag is probably not the most elegant solution but at the same time it's the less invasive way to implement it. I started trying to implement it creating a Pin object for a different object type acting as a view proxy but the code was overly complex and I ended up having the same need for a state variable.

Solution 1
It would require all the existing layout code to be refactored to opt-in to the feature plus it loose a bit of its simplicity that makes PinLayout so appealing IMO and like you said it would be easy to forget passing in the context object during pin creation.

Solution 2
I fear that the performance would take a huge hit on that one.

Solution 3
Well, I think it's the less bad solution and layout code should not be called from non main thread anyway, setting bounds or position on a background thread is forbidden. It might be a problem for another unknown Layoutable type that could potentially be layouted on multiple threads. In that case, my advise would be to not make that type conform to AutoSizeable.

The API is a bit similar to UIView.animate that wraps a beginAnimation() / endAnimation() behind the scene and a global flag is used to know wether or not the change should be performed animated or not. The fact that the API uses a closure where the layout code is expected to be called makes it less error prone I think.

@lucdion

Copy link
Copy Markdown
Member

I agree @antoinelamy, that your solution is probably the best, even if there is one flaw, its the one that has a minimal impact on code

@antoinelamy

Copy link
Copy Markdown
ContributorAuthor

Non main thread calls warnings

I would also add a warning when .pin is called from a thread other than the main thread (https://github.com/layoutBox/PinLayout#pinlayouts-warnings).
Ex:
⚠️ PinLayout should be used only from the main thread. UIKit calls must be called from the app's main thread.

This warning could be disabled Pin.activeWarnings.mainThread.

From what I see there is already such warning in PinLayout+Warning.displayLayoutWarnings() and it already covers the case of autosizing as we enter the apply() function the same way regular layout does:

if !Thread.isMainThread {
warn("Layout must be executed from the Main Thread!")
}

@antoinelamy
antoinelamy marked this pull request as ready for review June 16, 2020 11:49
@antoinelamyantoinelamy changed the title WIP: Add automatic sizeThatFits computation for viewsAdd automatic sizeThatFits computation for viewsJun 16, 2020
@antoinelamy

Copy link
Copy Markdown
ContributorAuthor

Should be good to go now @lucdion, anything else comes to mind?

Comment threadSources/Impl/PinLayout+Layouting.swift Outdated
@lucdion

Copy link
Copy Markdown
Member

Two more things:

  1. Could you duplicate the sample that you had previously modified? At least it shows an example.
  2. You won't like that, but even if its an experimental feature. We would need a minimalist documentation that describes the feature and how to use it. I would add that new section below https://github.com/layoutBox/PinLayout#justify--align
    Thanks

@antoinelamy

Copy link
Copy Markdown
ContributorAuthor

@lucdion I thought you preferred to silent release this feature, my mistake. I added a proper sample that fetches a random text and a random sized image. That content is then layout in a container that uses autosizing to determine its proper size. I also included setting a scroll view content size to illustrate the fact that we should not call non PinLayout code in the layout closure passed to autoSizeThatFits. I also added some documentation in the Readme for it, not sure if it's good enough but I'll leave it to you for review.

@lucdionlucdion left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ready to go 🎉
Thanks @antoinelamy for this nice addition

@lucdion
lucdion merged commit 21e53ef into layoutBox:masterJun 30, 2020
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@antoinelamy@lucdion