{"id":15170624,"url":"https://github.com/stevengharris/splitview","last_synced_at":"2025-05-02T22:31:21.690Z","repository":{"id":65696294,"uuid":"518231089","full_name":"stevengharris/SplitView","owner":"stevengharris","description":"A flexible way to split SwiftUI views with a draggable splitter","archived":false,"fork":false,"pushed_at":"2024-06-25T21:44:40.000Z","size":244,"stargazers_count":171,"open_issues_count":7,"forks_count":22,"subscribers_count":3,"default_branch":"main","last_synced_at":"2025-04-16T07:11:16.640Z","etag":null,"topics":["split","swift","swiftui"],"latest_commit_sha":null,"homepage":"","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/stevengharris.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2022-07-26T22:11:23.000Z","updated_at":"2025-04-12T03:13:45.000Z","dependencies_parsed_at":"2024-02-14T23:28:30.164Z","dependency_job_id":"ec6f9adf-8969-40ac-9549-59afd22aed5a","html_url":"https://github.com/stevengharris/SplitView","commit_stats":{"total_commits":41,"total_committers":1,"mean_commits":41.0,"dds":0.0,"last_synced_commit":"cee711f5e2f0dbbf5045dff85b45bc222eea9601"},"previous_names":[],"tags_count":25,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stevengharris%2FSplitView","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stevengharris%2FSplitView/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stevengharris%2FSplitView/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stevengharris%2FSplitView/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/stevengharris","download_url":"https://codeload.github.com/stevengharris/SplitView/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":252116244,"owners_count":21697342,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["split","swift","swiftui"],"created_at":"2024-09-27T08:04:17.537Z","updated_at":"2025-05-02T22:31:16.675Z","avatar_url":"https://github.com/stevengharris.png","language":"Swift","readme":"\u003cp align=\"center\"\u003e\n    \u003cimg src=\"https://img.shields.io/badge/Swift-5.6+-blue.svg\"\u003e\n    \u003cimg src=\"https://img.shields.io/badge/iOS-15.6+-blue.svg\" alt=\"iOS 15.6+\"\u003e\n    \u003cimg src=\"https://img.shields.io/badge/MacCatalyst-15.6+-blue\" alt=\"MacCatalyst 15.6+\"\u003e\n    \u003cimg src=\"https://img.shields.io/badge/Mac-12.4+-blue\" alt=\"MacCatalyst 12.4+\"\u003e\n    \u003ca href=\"https://mastodon.social/@stevengharris\"\u003e\n        \u003cimg src=\"https://img.shields.io/badge/Contact-@stevengharris-lightgrey.svg?style=flat\" alt=\"Mastodon: @stevengharris@mastodon.social\"\u003e\n    \u003c/a\u003e\n\u003c/p\u003e\n\n# SplitView\n\nThe Split, HSplit, and VSplit views and associated modifiers let you:\n\n* Create a single view containing two views, arranged in a horizontal (side-by-side) \nor vertical (above-and-below) `layout` separated by a draggable `splitter` for resizing.\n* Specify the `fraction` of full width/height for the initial position of the splitter.\n* Programmatically `hide` either view and change the `layout`.\n* Arbitrarily nest split views.\n* Constrain the splitter movement by specifying minimum fractions of the full width/height\nfor either or both views.\n* Drag-to-hide, so when you constrain the fraction on a side, you can hide the side \nwhen you drag more than halfway beyond the constraint.\n* Prioritize either of one the views to maintain its width/height as the containing \nview changes size.\n* Easily save the state of `fraction`, `layout`, and `hide` so a split view opens \nin its last state between application restarts.\n* Use your own custom `splitter` or the default Splitter.\n* Make splitters \"invisible\" (i.e., zero `visibleThickness`) but still draggable for \nresizing.\n* Monitor splitter movement in realtime, providing a simple way to create a custom slider.\n\n## Motivation\n\nNavigationSplitView is fine for a sidebar and for applications that conform to a \nnice master-detail type of model. On the other hand, sometimes you just need two \nviews to sit side-by-side or above-and-below each other and to adjust the split \nbetween them. You also might want to compose split views in ways that make sense \nin your own application context.\n\n## Demo\n\n![SplitView](https://user-images.githubusercontent.com/1020361/219515082-6e657bee-e4e2-4efd-9e78-f5c98aaa3083.mov)\n\nThis demo is available in the Demo directory as SplitDemo.xcodeproj. \n\n## Usage\n\nInstall the package.\n\n* To split two views horizontally, use an HSplit view.\n* To split two views vertically, use a VSplit view.\n* To split two views whose layout can be changed between horizontal and vertical, \nuse a Split view.\n\n**Note:** You can also use the `.split`, `.vSplit`, and `.hSplit` view modifiers that come \nwith the package to create a Split, VSplit, and HSplit view if that makes more sense to you.\nSee the discussion in [Style](#style).\n\nOnce you have created a Split, HSplit, or VSplit view, you can use view modifiers on them \nto:\n\n* Specify the initial fraction of the overall width/height that the left/top side should occupy.\n* Identify a side that can be hidden and unhidden.\n* Adjust the style of the default Splitter, including its color and thickness.\n* Place constraints on the minimum fraction each side occupies and which side should be\nprioritized (i.e., remain fixed in size) as the containing view's size changes.\n* Provide a custom splitter.\n* Be able to toggle layout between horizontal and vertical. This modifier is only \navailable for the Split view, since HSplit and VSplit remain in a horizontal or \nvertical layout by definition.\n\nIn its simplest form, the HSplit and VSplit views look like this:\n\n```\nHSplit(left: { Color.red }, right: { Color.green })\nVSplit(top: { Color.red }, bottom: { Color.green })\n```\n\nThe HSplit is a horizontal split view, evenly split between red on the left and \ngreen on the right. The VSplit is a vertical split view, evenly split between red \non the top and green on the bottom. Both views use a default splitter between them \nthat can be dragged to change the red and green view sizes.\n\nIf you want to set the the initial position of the splitter, you can use the \n`fraction` modifier. Here it is being used with a VSplit view:\n\n```\nVSplit(top: { Color.red }, bottom: { Color.green })\n    .fraction(0.25)\n```\n\nNow you get a red view above the green view, with the top occupying \n1/4 of the window.\n\nOften you want to hide/show one of the views you split. You can do this by specifying \nthe side to hide. Specify the side using a SplitSide. For an HSplit view, you can \nidentify the side using `.left` or `.right`. For a VSplit view, you can use `.top` \nor `.bottom`. For a Split view (where the layout can change), use `.primary` or \n`.secondary`. In fact, `.left`, `.top`, and `.primary` are all synonyms and can be \nused interchangably. Similarly, `.right`, `.bottom`, and `.secondary` are synonyms.\n\nHere is an HSplit view that hides the right side when it opens:\n\n```\nHSplit(left: { Color.red }, right: { Color.green })\n    .fraction(0.25)\n    .hide(.right)\n```\n\nThe green side will be hidden, but you can pull it open using the splitter that will \nbe visible on the right. This isn't usually what you want, though. Usually you want \nyour users to be able to control whether a side is hidden or not. To do this, pass the \nSideHolder ObservableObject that holds onto the side you are hiding. Similarly the SplitView \npackage comes with a FractionHolder and LayoutHolder. Under the covers, the Split view \nobserves all of these holders and redraws itself if they change. \n\nHere is an example showing how to use the SideHolder with a Button to hide/show the \nright (green) side:\n\n```\nstruct ContentView: View {\n    let hide = SideHolder()         // By default, don't hide any side\n    var body: some View {\n        VStack(spacing: 0) {\n            Button(\"Toggle Hide\") {\n                withAnimation {\n                    hide.toggle()   // Toggle between hiding nothing and hiding right\n                }\n            }\n            HSplit(left: { Color.red }, right: { Color.green })\n                .hide(hide)\n        }\n    }\n}\n```\n\nNote that the `hide` modifier accepts a SplitSide or a SideHolder. Similarly, `layout` \ncan be passed as a SplitLayout - `.horizontal` or `.vertical` - or as a LayoutHolder. \nAnd `fraction` can be passed as a CGFloat or as a FractionHolder.\n\nThe `toggle()` method on `hide` toggles the hide/show state for the `secondary` side \nby default. If you want to toggle the hide/show state for a specific side, then use \n`toggle(.primary)` or `toggle(.secondary)` explicitly. (Note that `.primary`, `.left`, \nand `.top` are synonyms; and `.secondary`, `.right`, and `.bottom` are synonyms.)\n\n### Nesting Split Views\n\nSplit views themselves can be split. Here is an example where the \nright side of an HSplit is a VSplit that has an HSplit at the bottom:\n\n```\nstruct ContentView: View {\n    var body: some View {\n        HSplit(\n            left: { Color.green },\n            right: {\n                VSplit(\n                    top: { Color.red },\n                    bottom: {\n                        HSplit(\n                            left: { Color.blue },\n                            right: { Color.yellow }\n                        )\n                    }\n                )\n            }\n        )\n    }\n}\n```\n\nAnd here is one where an HSplit contains two VSplits:\n\n```\nstruct ContentView: View {\n    var body: some View {\n        HSplit(\n            left: { \n                VSplit(top: { Color.red }, bottom: { Color.green })\n            },\n            right: {\n                VSplit(top: { Color.yellow }, bottom: { Color.blue })\n            }\n        )\n    }\n}\n```\n\n### Using UserDefaults For Split State\n\nThe three holders - SideHolder, LayoutHolder, and FractionHolder - all come with a \nstatic method to return instances that get/set their state from UserDefaults.standard. \nLet's expand the previous example to be able to change the `layout` and `hide` state \nand to get/set their values from UserDefaults. Note that if you want to adjust the \n`layout`, you need to use a Split view, not HSplit or VSplit. We create the Split view \nby specifying the `primary` and `secondary` views. When the SplitLayout held by the\nLayoutHolder (`layout`) is `.horizontal`, the `primary` view is on the left side, and \nthe `secondary` view is on the right.  When the SplitLayout toggles to `vertical`, the \n`primary` view is on the top, and the `secondary` view is on the bottom.\n\n```\nstruct ContentView: View {\n    let fraction = FractionHolder.usingUserDefaults(0.5, key: \"myFraction\")\n    let layout = LayoutHolder.usingUserDefaults(.horizontal, key: \"myLayout\")\n    let hide = SideHolder.usingUserDefaults(key: \"mySide\")\n    var body: some View {\n        VStack(spacing: 0) {\n            HStack {\n                Button(\"Toggle Layout\") {\n                    withAnimation {\n                        layout.toggle()\n                    }\n                }\n                Button(\"Toggle Hide\") {\n                    withAnimation {\n                        hide.toggle()\n                    }\n                }\n            }\n            Split(primary: { Color.red }, secondary: { Color.green })\n                .fraction(fraction)\n                .layout(layout)\n                .hide(hide)\n        }\n    }\n}\n```\n\nThe first time you open this, the sides will be split 50-50, but as you drag the \nsplitter, the `fraction` state is also retained in UserDefaults.standard.\nYou can change the `layout` and hide/show the green view, and when you next open \nthe app, the `fraction`, `hide`, and `layout` will all be restored how you left them.\n\n### Modifying And Constraining The Default Splitter \n\nYou can change the way the default Splitter displays using the `styling` modifier. \nFor example, you can change the color, inset, and thickness:\n\n```\nHSplit(left: { Color.red }, right: { Color.green })\n    .fraction(0.25)\n    .styling(color: Color.cyan, inset: 4, visibleThickness: 8)\n```\n\nIf you prefer the splitter to hide also when you hide a side, you can set `hideSplitter`\nto `true` in the `styling` modifier. For example:\n\n```\nHSplit(left: { Color.red }, right: { Color.green })\n    .styling(hideSplitter: true)\n```\n\nNote that if you set `hideSplitter` to `true`, you need to include a means for \nyour user to unhide a view once it is hidden, like a hide/show button. That's \nbecause the splitter itself isn't displayed at all, so you can't just drag it out \nfrom the side.\n\nBy default, the splitter can be dragged across the full width/height of the split \nview. The `constraints` modifier lets you constrain the minimum faction of the \noverall view that the \"primary\" and/or \"secondary\" view occupies, so the \nsplitter always stays within those constraints. You can do this by specifying \n`minPFraction` and/or `minSFraction`. The `minPFraction` refers to left \nin HSplit and top in VSplit, while `minSFraction` refers to right in HSplit and \nbottom in VSplit:\n\n```\nHSplit(left: { Color.red }, right: { Color.green })\n    .fraction(0.3)\n    .constraints(minPFraction: 0.2, minSFraction: 0.2)\n```\n\n### Drag-To-Hide\n\nWhen you constrain the fraction of the primary or secondary side, you may want the \nside to hide automatically when you drag past the constraint. However, we need to \ntrigger this drag-to-hide behavior when you drag \"well past\" the constraint, because \notherwise, it's difficult to leave the splitter positioned at the constraint without\nhiding it. For this reason, a split view defines \"well past\" to mean \"more than \nhalfway past the contraint\".\n\nDrag-to-hide can be a nice shortcut to avoid having to press a button to hide a side. \nYou can see an example of it in Xcode when you drag the splitter between the editor area \nin the middle and the Inspector on the right beyond the constraint Xcode puts on the \nInspector width. In Xcode, when you drag-to-hide the splitter between the editor area \nand the Inspector, you cannot drag it back out because the splitter itself is hidden. \nYou need a button to invoke the hide/show action, as discussed \n[earlier](#modifying-and-constraining-the-default-splitter). The same is true with \ndrag-to-hide using a split view when `hideSplitter` is `true`.\n\nWhen your cursor moves beyond the halfway point of the constrained side, the split view \npreviews what it will look like when the side is hidden. This way, you have a visual indication \nthat the side will hide, and you can drag back out to avoid hiding it. If your dragging ends \nwhen the side is hidden, then it will remain hidden.\n\nNote that when you use drag-to-hide, the splitter may or may not be hidden when the side is \nhidden (depending on whether `hideSplitter` is `true` in SplitStyling). The preview of what the \nsplit view will look like if you release past the halfway point reflects your choice of setting \nfor `hideSplitter`.\n\nTo use drag-to-hide, add `dragToHideP` and/or `dragToHideS` to your `constraints` definition.\nFor example, the following will constrain dragging between 20% and 80% of the width, but \nwhen the drag gesture ends at or beyond the 90% mark on the right, the secondary side will \nhide. Note also that in this case, the primary side doesn't use drag-to-hide:\n\n```\nHSplit(left: { Color.red }, right: { Color.green })\n    .constraints(minPFraction: 0.2, minSFraction: 0.2, dragToHideS: true)\n```\n\n### Custom Splitters\n\nBy default the Split, HSplit, and VSplit views all use the default Splitter view. You can \ncreate your own and use it, though. Your custom splitter should conform to SplitDivider \nprotocol, which makes sure your custom splitter can let the Split view know what its \n`styling` is. The `styling.visibleThickness` is the size your custom splitter displays \nitself in, and it also defines the spacing between the `primary` and `secondary` views inside \nof Split view.\n\nThe Split view detects drag events occurring in the splitter. For this reason, you might want \nto use a ZStack with an underlying Color.clear that represents the `styling.invisibleThickness`\nif the `styling.visibleThickness` is too small for properly detecting the drag events.\n\nHere is an example custom splitter whose contents is sensitive to the observed `layout` \nand `hide` state:\n\n```\nstruct CustomSplitter: SplitDivider {\n    @ObservedObject var layout: LayoutHolder\n    @ObservedObject var hide: SideHolder\n    @ObservedObject var styling: SplitStyling\n    /// The `hideButton` state tells whether the custom splitter hides the button that normally shows\n    /// in the middle. If `styling.previewHide` is true, then we only want to show the button if\n    /// `styling.hideSplitter` is also true.\n    /// In general, people using a custom splitter need to handle the layout when `previewHide`\n    /// is triggered and that layout may depend on whether `hideSplitter` is `true`.\n    @State private var hideButton: Bool = false\n    let hideRight = Image(systemName: \"arrowtriangle.right.square\")\n    let hideLeft = Image(systemName: \"arrowtriangle.left.square\")\n    let hideDown = Image(systemName: \"arrowtriangle.down.square\")\n    let hideUp = Image(systemName: \"arrowtriangle.up.square\")\n    \n    var body: some View {\n        if layout.isHorizontal {\n            ZStack {\n                Color.clear\n                    .frame(width: 30)\n                    .padding(0)\n                if !hideButton {\n                    Button(\n                        action: { withAnimation { hide.toggle() } },\n                        label: {\n                            hide.side == nil ? hideRight.imageScale(.large) : hideLeft.imageScale(.large)\n                        }\n                    )\n                    .buttonStyle(.borderless)\n                }\n            }\n            .contentShape(Rectangle())\n            .onChange(of: styling.previewHide) { hide in\n                hideButton = styling.hideSplitter\n            }\n        } else {\n            ZStack {\n                Color.clear\n                    .frame(height: 30)\n                    .padding(0)\n                if !hideButton {\n                    Button(\n                        action: { withAnimation { hide.toggle() } },\n                        label: {\n                            hide.side == nil ? hideDown.imageScale(.large) : hideUp.imageScale(.large)\n                        }\n                    )\n                    .buttonStyle(.borderless)\n                }\n            }\n            .contentShape(Rectangle())\n            .onChange(of: styling.previewHide) { hide in\n                hideButton = styling.hideSplitter\n            }\n        }\n    }}\n``` \n\nYou can use the CustomSplitter like this:\n\n```\nstruct ContentView: View {\n    let layout = LayoutHolder()\n    let hide = SideHolder()\n    let styling = SplitStyling(visibleThickness: 20)\n    var body: some View {\n        Split(primary: { Color.red }, secondary: { Color.green })\n            .layout(layout)\n            .hide(hide)\n            .splitter { CustomSplitter(layout: layout, hide: hide, styling: styling) }\n    }\n}\n```\n\nIf you make a custom splitter that would be generally useful to people, consider filing \na pull request for an additional Splitter extension in Splitter+Extensions.swift. \nThe `line` Splitter is included in the file as an example that is used in the \"Sidebars\" \ndemo. Similarly, the `invisible` Splitter re-uses the `line` splitter by passing a \n`visibleThickness` of zero and is used in the \"Invisible splitter\" demo.\n\n### Invisible Splitters\n\nYou might want the views you split to be adjustable using the splitter, but for the splitter \nitself to be invisible. For example, a \"normal\" sidebar doesn't show a splitter between itself \nand the detail view it sits next to. You can do this by passing `Splitter.invisible()` as the \ncustom splitter.\n\nOne thing to watch out for with an invisible splitter is that when a side is hidden, there \nis no visual indication that it can be dragged back out. To prevent this issue, you should \nspecify `minPFraction` and `minSFraction` when using `Splitter.invisible()`.\n\n```\nstruct ContentView: View {\n    let hide = SideHolder()\n    var body: some View {\n        VStack(spacing: 0) {\n            Button(\"Toggle Hide\") {\n                withAnimation {\n                    hide.toggle()   // Toggle between hiding nothing and hiding secondary\n                }\n            }\n            HSplit(left: { Color.red }, right: { Color.green })\n                .hide(hide)\n                .constraints(minPFraction: 0.2, minSFraction: 0.2)\n                .splitter { Splitter.invisible() }\n        }\n    }\n}\n```\n\n### Monitoring And Responding To Splitter Movement\n\nYou can specify a callback for the split view to execute as you drag the splitter. The \ncallback reports the `privateFraction` being tracked; i.e., the fraction of the full \nwidth/height occupied by the left/top side. Specify the callback using the `onDrag(_:)`\nmodifier for any of the split views. \n\nHere is an example of a DemoSlider that uses the `onDrag(_:)` modifier to update \na Text view showing the percentage each side is occupying.\n\n```\nstruct DemoSlider: View {\n    @State private var privateFraction: CGFloat = 0.5\n    var body: some View {\n        HSplit(\n            left: {\n                ZStack {\n                    Color.green\n                    Text(percentString(for: .left))\n                }\n            },\n            right: {\n                ZStack {\n                    Color.red\n                    Text(percentString(for: .right))\n                }\n            }\n        )\n        .onDrag { fraction in privateFraction = fraction }\n        .frame(width: 400, height: 30)\n    }\n\n    /// Return a string indicating the percentage occupied by `side`\n    func percentString(for side: SplitSide) -\u003e String {\n        var percent: Int\n        if side.isPrimary {\n            percent = Int(round(100 * privateFraction))\n        } else {\n            percent = Int(round(100 * (1 - privateFraction)))\n        }\n        // Empty string if the side will be too small to show it\n        return percent \u003c 10 ? \"\" : \"\\(percent)%\"\n    }\n}\n```\n\nIt looks like this:\n\n![DemoSlider](https://user-images.githubusercontent.com/1020361/231880861-c710dfb8-ada3-41e2-802b-a71d947b867f.mov)\n\n### Prioritizing The Size Of A Side\n\nWhen you want a sidebar type of arrangement using HSplit views, you often want \nthe sidebar to maintain its width as you resize the overall view. You might \nhave the same need with a VSplit, too. If you have two sidebars, you may want \nto slide either one while the opposing one stays the same width. You can \naccomplish this by specifying a `priority` side (either `.left`/`.right` or \n`.top`/`.bottom`) in the `constraints` modifier.\n\nHere is an example that has a red left sidebar and green right sidebar surrounding a \nyellow middle view. As you drag either splitter, the other stays fixed. Under the covers, \nthe Split view is adjusting the proportion between `primary` and `secondary` to keep the \nsplitter in the same place. You will also see that as you resize the window, both \nsidebars maintain their width.\n\n```\nstruct ContentView: View {\n    var body: some View {\n        HSplit(\n            left: { Color.red },\n            right: {\n                HSplit(\n                    left: { Color.yellow },\n                    right: { Color.green }\n                )\n                .fraction(0.75)\n                .constraints(priority: .right)\n            }\n        )\n        .fraction(0.2)\n        .constraints(priority: .left)\n    }\n}\n```\n\nNote that in the example above, the two sidebars have the same width, \nwhich is 0.2 of the overall width, even though the fractions specified for the \nleft and right sides are 0.2 and 0.75 respectively. This is because the left side \nof the outer HSplit is 0.2 of the overall width, leaving 0.8 to divide in the inner \nHSplit. The left side of the inner HSplit is 0.75\\*0.8 or 0.6 of the overall width, \nleaving the right side of the inner HSplit to be 0.2 of the overall width.\n\n## Implementation\n\nThe heart of the implementation here is the Split view. VSplit and HSplit are really \nconvenience and clarity wrappers around Split. There is probably not a big need for \nmost people to be able to adjust layout dynamically, which is really the only reason \nto use Split directly.\n\nAlthough ultimately Split has to deal in width and height, the math of adjusting the \nlayout is the same whether its `primary` is at the left or top and its `secondary` is \nat the right or bottom.\n\nThe main piece of state that changes in Split view is `constrainedFraction`. This is the \nfraction of the overall width/height occupied by the `primary` view. It changes as you \ndrag the splitter. When you hide/show, it does not change, because it holds the state \nneeded to restore-to when a hidden view is shown again. The Split view monitors changes \nto its size. The size changes when its containing view changes size (e.g., resizing a \nwindow on the Mac or when nested in another Split view whose splitter is dragged).\n\nThe three views, Split, HSplit, and VSplit all support the same modifiers \nto adjust `fraction`, `hide`, `styling`, `constraints`, `onDrag`, and `splitter`. \nThe Split view also has a modifier for `layout` (which is also used by HSplit and \nVSplit) and a few convenience modifiers used by HSplit and VSplit.\n\n### Style\n\nAfter going all-in on a View modifier style to return a single Split-type of view \nfor any View it is invoked on, I read an \n[article by John Sundell](https://swiftbysundell.com/articles/swiftui-views-versus-modifiers/) \nthat illustrated some of the \"problematic\" issues associated with view modifiers \ncreating different container views. As a result, I reconsidered my approach. \nI'm still using view modifiers extensively, but now they operate on an explicit \nSplit, HSplit, or VSplit container, and always return the same type of view they \nmodify. I think this makes usage a lot more clear in the end. \n\nIf you prefer the idea of a View modifier to kick off your Split, HSplit, or VSplit \ncreation, you can still use:\n\n```\nColor.green.hSplit { Color.red }   // Returns an HSplit\nColor.green.vSplit { Color.red }   // Returns a VSplit\nColor.green.split { Color.red }    // Returns a Split\n```\n\ninstead of:\n\n```\nHSplit(left: { Color.green }, right: { Color.red } )\nVSplit(top: { Color.green }, bottom: { Color.red } )\nSplit(primary: { Color.green }, secondary: { Color.red })\n```\n\n## Issues\n\n1. In versions prior to MacOS 14.0 Sonoma, there is what appears to be a harmless\nlog message when dragging the Splitter to cause a view size to go to zero on \nMac Catalyst only. The message shows up in the Xcode console as `[API] cannot \nadd handler to 3 from 3 - dropping`. This message is not present as of MacOS 14.0 Sonoma.\n\n2. The Splitter's `onHover` entry action used to display the resizing cursors on Mac Catalyst \nand MacOS may occasionally not be triggered when using nested split views. I think this happens \nseldom enough to not be a problem. When it occurs, the cursor doesn't change to \n`resizeLeftRight` or `resizeUpDown` when hovering over a splitter, but the splitter will \nstill be draggable.\n\n## Possible Enhancements\n\nI might add a few things but would be very happy to accept pull requests! For example,\na split view that adapted to device orientation and form factors somewhat like \nNavigationSplitView would be useful.\n\n## History\n\n### Version 3.5\n\n* Publish changes to `fraction`, so that setting the value externally changes the split view layout (Issue [29](https://github.com/stevengharris/SplitView/issues/29)).\n* Allow use of `toggle(.primary)` or `toggle(.secondary)` as a way to specify the side to hide/show (thanks [Bastiaan Terhorst](https://github.com/bastiaanterhorst)).\n* Fix display bug when specifying `minFractionP` and hiding `primary` side (Issue [31](https://github.com/stevengharris/SplitView/issues/31)).\n* Remove forced hiding of splitter when hiding a side with `minFractionP` or `minFractionS` specified (Issue [30](https://github.com/stevengharris/SplitView/issues/30)).\n* Update README to reflect changes.\n\n### Version 3.4\n\n* Refactor so that Splitter holds SplitStyling, allowing custom splitters to participate properly in drag-to-hide.\n* Incompatible change to SplitDivider protocol to expose `styling: SplitStyling` rather than `visibleThickness`. The incompatibility only affects you if you were using a [custom splitter](#custom-splitters).\n\n### Version 3.3\n\n* Support drag-to-hide with a preview of the side being hidden as you drag beyond the \nhalfway point of the constrained side. See the [Drag-To-Hide](#drag-to-hide) section.\n\n### Version 3.2\n\n* Display resizing cursors on Mac Catalyst and MacOS when hovering over the splitter.\n* Add ability to hide a side when dragging completes at a point beyond the minimum constraints. See the [Drag-To-Hide](#drag-to-hide) section.\n* Add ability to hide the splitter when a side is hidden. See the information on `hideSplitter` in the [Modifying And Constraining The Default Splitter](#modifying-and-constraining-the-default-splitter) section.\n\n### Version 3.1\n\n* Add `onDrag` modifier to be able to monitor and respond to splitter movement. See the [Monitoring And Responding To Splitter Movement](#monitoring-and-responding-to-splitter-movement) section.\n\n### Version 3.0\n\n* Incompatible change from Version 2 to change from an extensive set of View modifiers to explicit use of Split, HSplit, and VSplit. Most of the previous version's `split` View modifiers have been removed in this version.\n* Modify the DemoApp to use the new Split, HSplit, and VSplit approach. Functionality is unchanged.\n\n### Version 2.0\n\n* Incompatible change from Version 1 in split enums. SplitLayout cases change from `.Horizontal` and `.Vertical` to `.horizontal` and `.vertical`. SplitSide cases change from `.Primary` and `.Secondary` to `.primary` and `.secondary`.\n* Add ability to specify a side (`.primary` or `.secondary`) that has sizing `priority`. The size of the `priority` side remains unchanged as its containing view resizes. If `priority` is not specified - the default - then the proportion between `primary` and `secondary` is maintained. This enables proper sidebar type of behavior, where changing one sidebar's size does not affect the other.\n* Add a sidebar demo showing the use of `priority`.\n\n### Version 1.1\n\n* Generalize the way configuration of SplitView properties are handled using SplitConfig, which can optionally be passed to the `split` modifier. \nThere is a minor compatibility change in that properties such as `color` and `visibleThickness` must be passed to the default Splitter using SplitConfig.\n* Allow minimum fractions - `minPFraction` and `minSFraction` - to be configured in SplitConfig to constrain the size of the `primary` and/or `secondary` views.\n* If a minimum fraction is specified for a side and that side is hidden, then the splitter will be hidden, too. The net effect of this change is \nthat the hidden side cannot be dragged open when it is hidden and a minimum fraction is specified for a side. It can still be unhidden by \nchanging its SideHolder. Under these conditions, the unhidden side occupies the full width/height when the other is hidden, without any inset \nfor the splitter.\n\n### Version 1.0\n\nMake layout adjustable. Clean up and formalize the SplitDemo, including the custom splitter and \"invisible\" splitter. Update the README.\n\n### Version 0.2\n\nEliminates the use of the clear background and SizePreferenceKeys. (My suspicion is they were needed earlier because GeometryReader otherwise caused bad behavior, but in any case they are not needed now.) Eliminate HSplitView and VSplitView, which were themselves holding onto a SplitView. The layering was both unnecessary and not adding value other than making it explicit what kind of SplitView was being created. I concluded that the same expression was actually clearer and more concise using ViewModifiers. I also added the Example.xcworkspace.\n\n### Version 0.1\n\nOriginally posted in [response](https://stackoverflow.com/a/68926261) to https://stackoverflow.com/q/67403140. This version used HSplitView and VSplitView as a means to create the SplitView. It also used SizePreferenceKeys from a GeometryReader on a clear background to set the size. In nested SplitViews, I found this was causing \"Bound preference ... tried to update multiple times per frame\" to happen intermittently depending on the view arrangement.\n\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fstevengharris%2Fsplitview","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fstevengharris%2Fsplitview","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fstevengharris%2Fsplitview/lists"}