The Apple II employs what's called a geographic addressing. That is each expansion slot gets a fixed address (range) reserved and assigned for local resources. Thus the address of a component (like a VIA) changes, depending on which slot is used. Geographic addressing is the most simple form of plug and play while retaining the ability to have multiple instances of a certain I/O device.
One primary goal of the Apple II I/O design was to enable card builders to create new cards with the least possible effort. Geographic addressing allows this, as there is, at least for simple cards, no need for additional decoding on the card, as the slot specific select line is all that needs to be used.
For example a 6522 VIA card could be built without any additional circuitry but the chip itself. Slot lines could be directly wired up to the IC with the lower 4 address lines tied to the register address input and
/DEVSEL to chip enable. Similarly most other I/O chips of that time could be added.
For such simple I/O, each slot got a 16 byte address range for registers set aside. In a more general view it is preferable if another address range is available for firmware code, so cards could bring drivers in ROM for instant usage. Since the 64 KiB address range of a 6502 doesn't allow big chunks of memory to be set aside - and many drivers won't need more than a few bytes anyway, each slot did get a additional 256 byte address range assigned. This was usually used for driver code and called 'Peripheral Card ROM Space'. Access was signaled by another select line called
/IOSEL. So if a card did bring driver code, all it needed was to fit it in a ((E)P)ROM and have address, data and CE line tied to the respective slot lines. Again, no additional hardware needed.
256 bytes may not sound like much, but be assured, it is quite a lot. There where many I/O cards where the whole driver didn't even need that much space. Keep in mind, Woz managed to put the whole boot loader code for the Disk II controller into a 256 bytes - and that includes all necessary drive handling. There is a nice email from Steve Wozniak preserved, where he not only described that he did put a 'whole' monitor program for a pre-Apple 1 prototype into 256 bytes of PROM, but also mentions his 256 byte printer driver for the Apple II.
In case a card needs to offer much more complicated driver code, which couldn't fit in 256 bytes (or a multiple using some switching), another 2 KiB area, now shared by all cards, signaled by
/IOSTROBE, was set aside. This sharing was time sliced - meaning that only one card could have its hardware visible at a given time. Therefore a hardware based protocol for activation and deactivation was needed. Whenever a driver needed access to address space, like for more code in a ROM, it first needed to deselect whatever was active before and then map in its own code. To do this all cards watched address
$CFFF and deselected their ROM when it got accessed. To enable each card could use whatever private method. Here for the first time hardware beyond the device in question (like ROM) was needed. Still only one flip-flop and a wide AND gate.
All of this together (including all on board resources) resulted in a 4 KiB I/O space within the Apple II memory map. Located at $C000-$CFFF it looked like this:
$C000..C07F On Board Resources
$C080..C08F Slot 0 /DEVSEL area (16 byte register file)
$C090..C09F Slot 1 /DEVSEL area
... repeated for Slot 2..6
$C0F0..C0FF Slot 7 /DEVSEL
$C100..C1FF Slot 1 /IOSEL area (256 bytes 'PROM')
... repeated for Slot 2..6
$C700..C7FF Slot 7 /IOSEL area
$C800..CFFF Common area for all Slots (2 KiB 'ROM')
Now it's also easy to see why slot 0 could only house certain cards, as its 'PROM' area was used for the internal I/O and the register windows of all other slots.
These great preparations for simple I/O card design are for sure part of the success the Apple II had. From a hardware point it was extremely easy to enhance the system.
In addition to the hardware definitions a simple software protocol was added to make use of the driver code from BASIC, as well from assembly. In general
IN#n did set the output or input vector (CSW/KSW) to
$Cn00, thus handing over data to the cards routines or taking it from there. Cards with dual functions did need to manipulate the vectors accordingly. Later on, with the Pascal system, a more sophisticated protocol of card IDs, and multiple entry points got developed. But that's a different story.